mirror of
https://github.com/go-gitea/gitea.git
synced 2026-11-05 00:09:33 +09:00
fix(web): allow clearing the full name in profile settings (#40186)
Clearing "Full Name" in the profile settings shows success but keeps the old value: since #31959 it is only updated when non-empty, because the input is disabled (not submitted) when `change_full_name` is disabled. Use `*string` for the field, so a missing field keeps the value and an empty one clears it. With `change_full_name` disabled, any submitted value (including empty) is now rejected. Not `optional.Option[string]`, because `MaxSize` would count slice elements instead of characters. Fixes #40185 --- AI disclosure: written with Claude Code (claude-opus-5-5) and tested locally. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
10 files changed
+206
-193
No files matched your search
@@ -11,6 +11,8 @@ import "strconv"
|
||||
// * non-existing value are represented as an empty slice (None)
|
||||
// * existing value is represented as a slice with one element (Some)
|
||||
// * multiple values are represented as a slice with multiple elements (Some), the Value is the first element (not well-defined in this case)
|
||||
//
|
||||
// Attention: Option[T] doesn't work with some web middleware binding rules like "MaxSize" at the moment.
|
||||
type Option[T any] []T
|
||||
|
||||
func None[T any]() Option[T] {
|
||||
|
||||
@@ -54,71 +54,55 @@ func Profile(ctx *context.Context) {
|
||||
|
||||
// ProfilePost response for change user's profile
|
||||
func ProfilePost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings_title")
|
||||
ctx.Data["PageIsSettingsProfile"] = true
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
ctx.Data["DisableGravatar"] = setting.Config().Picture.DisableGravatar.Value(ctx)
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplSettingsProfile)
|
||||
form := context.GetFetchActionForm[*forms.UpdateProfileForm](ctx)
|
||||
if form == nil {
|
||||
return
|
||||
}
|
||||
|
||||
form := web.GetForm[*forms.UpdateProfileForm](ctx)
|
||||
|
||||
if form.Name != "" {
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeUsername) {
|
||||
ctx.Flash.Error(ctx.Tr("user.form.change_username_disabled"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.change_username_disabled"), "Name")
|
||||
return
|
||||
}
|
||||
if err := user_service.RenameUser(ctx, ctx.Doer, form.Name, ctx.Doer); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrUserIsNotLocal(err):
|
||||
ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user"))
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.username_change_not_local_user"), "Name")
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.username_been_taken"), "Name")
|
||||
case db.IsErrNameReserved(err):
|
||||
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", form.Name))
|
||||
ctx.JSONErrorWithField(ctx.Tr("user.form.name_reserved", form.Name), "Name")
|
||||
case db.IsErrNamePatternNotAllowed(err):
|
||||
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", form.Name))
|
||||
ctx.JSONErrorWithField(ctx.Tr("user.form.name_pattern_not_allowed", form.Name), "Name")
|
||||
case db.IsErrNameCharsNotAllowed(err):
|
||||
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", form.Name))
|
||||
ctx.JSONErrorWithField(ctx.Tr("user.form.name_chars_not_allowed", form.Name), "Name")
|
||||
default:
|
||||
ctx.ServerError("RenameUser", err)
|
||||
return
|
||||
ctx.JSONErrorAuto(err)
|
||||
}
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
if form.FullName != nil && user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
|
||||
ctx.JSONErrorWithField(ctx.Tr("form.change_full_name_disabled"), "FullName")
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.Doer, &user_service.UpdateOptions{
|
||||
KeepEmailPrivate: optional.Some(form.KeepEmailPrivate),
|
||||
FullName: optional.FromPtr(form.FullName),
|
||||
Description: optional.Some(form.Description),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
Visibility: optional.Some(form.Visibility),
|
||||
KeepActivityPrivate: optional.Some(form.KeepActivityPrivate),
|
||||
}
|
||||
|
||||
if form.FullName != "" {
|
||||
if user_model.IsFeatureDisabledWithLoginType(ctx.Doer, setting.UserFeatureChangeFullName) {
|
||||
ctx.Flash.Error(ctx.Tr("user.form.change_full_name_disabled"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
return
|
||||
}
|
||||
opts.FullName = optional.Some(form.FullName)
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.Doer, opts); err != nil {
|
||||
ctx.ServerError("UpdateUser", err)
|
||||
}); err != nil {
|
||||
ctx.JSONErrorAuto(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("User settings updated: %s", ctx.Doer.Name)
|
||||
ctx.Flash.Success(ctx.Tr("settings.update_profile_success"))
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings")
|
||||
ctx.JSONRedirect(setting.AppSubURL + "/user/settings")
|
||||
}
|
||||
|
||||
// UpdateAvatarSetting update user's avatar
|
||||
|
||||
+1
-1
@@ -635,7 +635,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
|
||||
m.Group("/user/settings", func() {
|
||||
m.Get("", user_setting.Profile)
|
||||
m.Post("", web.Bind[*forms.UpdateProfileForm](), user_setting.ProfilePost)
|
||||
m.Post("", user_setting.ProfilePost)
|
||||
m.Post("/update_preferences", user_setting.UpdatePreferences)
|
||||
m.Get("/change_password", auth.MustChangePassword)
|
||||
m.Post("/change_password", web.Bind[*forms.MustChangePasswordForm](), auth.MustChangePasswordPost)
|
||||
|
||||
@@ -160,8 +160,8 @@ type IntrospectTokenForm struct {
|
||||
// UpdateProfileForm form for updating profile
|
||||
type UpdateProfileForm struct {
|
||||
middleware.FormDefaultValidator
|
||||
Name string `binding:"Username;MaxSize(40)"`
|
||||
FullName string `binding:"MaxSize(100)"`
|
||||
Name string `binding:"Username;MaxSize(40)"`
|
||||
FullName *string `binding:"MaxSize(100)"`
|
||||
KeepEmailPrivate bool
|
||||
Website string `binding:"ValidSiteUrl;MaxSize(255)"`
|
||||
Location string `binding:"MaxSize(50)"`
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
<p>{{ctx.Locale.Tr "settings.profile_desc"}}</p>
|
||||
<form class="ui form" action="{{.Link}}" method="post" data-global-init="initUserSettingsForm">
|
||||
<form class="ui form form-fetch-action" action="{{.Link}}" method="post" data-global-init="initUserSettingsForm">
|
||||
<div class="required field {{if .Err_Name}}error{{end}}">
|
||||
<label for="username">{{ctx.Locale.Tr "username"}}
|
||||
<span class="tw-text-red tw-hidden" id="name-change-prompt"> {{ctx.Locale.Tr "settings.change_username_prompt"}}</span>
|
||||
|
||||
@@ -9,9 +9,11 @@ test('update profile biography', async ({page, request}) => {
|
||||
await page.goto('/user/settings');
|
||||
await page.getByLabel('Biography').fill(bio);
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByText('Your profile has been updated.', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue(bio);
|
||||
await page.getByLabel('Biography').fill('');
|
||||
await page.getByRole('button', {name: 'Update Profile'}).click();
|
||||
await expect(page.getByText('Your profile has been updated.', {exact: true})).toBeVisible();
|
||||
await expect(page.getByLabel('Biography')).toHaveValue('');
|
||||
});
|
||||
|
||||
|
||||
@@ -785,7 +785,7 @@ func TestPackageContainer(t *testing.T) {
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
t.Run(fmt.Sprintf("Catalog[%s]", newOwnerName), checkCatalog(newOwnerName))
|
||||
|
||||
@@ -794,6 +794,6 @@ func TestPackageContainer(t *testing.T) {
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
@@ -53,7 +53,7 @@ func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) {
|
||||
"language": "en-US",
|
||||
"keep_activity_private": "1",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc *HTMLDoc) bool {
|
||||
|
||||
@@ -10,14 +10,36 @@ import (
|
||||
|
||||
auth_model "gitea.dev/models/auth"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/container"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestUserSettings(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("RevokeOAuth2GrantOfOtherUser", testRevokeOAuth2GrantOfOtherUser)
|
||||
t.Run("Account", testUserSettingsAccount)
|
||||
t.Run("UpdatePassword", testUserSettingsUpdatePassword)
|
||||
t.Run("UpdateEmail", testUserSettingsUpdateEmail)
|
||||
t.Run("DeleteEmail", testUserSettingsDeleteEmail)
|
||||
t.Run("Delete", testUserSettingsDelete)
|
||||
t.Run("Pages", testUserSettingsPages)
|
||||
t.Run("Security", testUserSettingsSecurity)
|
||||
t.Run("Applications", testUserSettingsApplications)
|
||||
t.Run("Keys", testUserSettingsKeys)
|
||||
t.Run("Secrets", testUserSettingsSecrets)
|
||||
t.Run("RenameInvalidUsername", testRenameInvalidUsername)
|
||||
t.Run("RenameReservedUsername", testRenameReservedUsername)
|
||||
t.Run("ChangeFullName", testChangeFullName)
|
||||
t.Run("LocationMapLink", testUserLocationMapLink)
|
||||
t.Run("RenameUsername", testRenameUsername)
|
||||
}
|
||||
|
||||
// Validate that each navbar setting is correct. This checks that the
|
||||
// appropriate context is passed everywhere the navbar is rendered
|
||||
func assertNavbar(t *testing.T, doc *HTMLDoc) {
|
||||
@@ -56,9 +78,7 @@ func WithDisabledFeatures(t *testing.T, features ...string) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevokeOAuth2GrantOfOtherUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testRevokeOAuth2GrantOfOtherUser(t *testing.T) {
|
||||
// grant 1 belongs to user1, so user2 must not be able to act on it
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/applications/oauth2/1/revoke/1", nil)
|
||||
@@ -68,9 +88,7 @@ func TestRevokeOAuth2GrantOfOtherUser(t *testing.T) {
|
||||
assert.EqualValues(t, 1, grant.UserID)
|
||||
}
|
||||
|
||||
func TestUserSettingsAccount(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsAccount(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
features []string
|
||||
@@ -123,9 +141,7 @@ func TestUserSettingsAccount(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsUpdatePassword(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsUpdatePassword(t *testing.T) {
|
||||
t.Run("enabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -148,9 +164,7 @@ func TestUserSettingsUpdatePassword(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsUpdateEmail(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsUpdateEmail(t *testing.T) {
|
||||
t.Run("credentials disabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -183,9 +197,7 @@ func TestUserSettingsUpdateEmail(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserSettingsDeleteEmail(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsDeleteEmail(t *testing.T) {
|
||||
t.Run("credentials disabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -196,9 +208,7 @@ func TestUserSettingsDeleteEmail(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsDelete(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsDelete(t *testing.T) {
|
||||
t.Run("deletion disabled", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -210,10 +220,10 @@ func TestUserSettingsDelete(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsPages(t *testing.T) {
|
||||
func testUserSettingsPages(t *testing.T) {
|
||||
for _, page := range []string{"appearance", "packages", "packages/rules/add", "organization", "repos", "blocked_users"} {
|
||||
t.Run(page, func(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user/settings/"+page)
|
||||
@@ -225,9 +235,7 @@ func TestUserSettingsPages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserSettingsSecurity(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsSecurity(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
feature string
|
||||
@@ -261,9 +269,7 @@ func TestUserSettingsSecurity(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsApplications(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsApplications(t *testing.T) {
|
||||
t.Run("Applications", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
@@ -342,9 +348,7 @@ func TestUserSettingsApplications(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsKeys(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsKeys(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
name string
|
||||
feature string
|
||||
@@ -384,9 +388,7 @@ func TestUserSettingsKeys(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserSettingsSecrets(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
func testUserSettingsSecrets(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequest(t, "GET", "/user/settings/actions/secrets")
|
||||
if setting.Actions.Enabled {
|
||||
@@ -398,3 +400,144 @@ func TestUserSettingsSecrets(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func testRenameInvalidUsername(t *testing.T) {
|
||||
invalidUsernames := []string{
|
||||
"%2f*",
|
||||
"%2f.",
|
||||
"%2f..",
|
||||
"%00",
|
||||
"thisHas ASpace",
|
||||
"p<A>tho>lo<gical",
|
||||
".",
|
||||
"..",
|
||||
".well-known",
|
||||
".abc",
|
||||
"abc.",
|
||||
"a..bc",
|
||||
"a...bc",
|
||||
"a.-bc",
|
||||
"a._bc",
|
||||
"a_-bc",
|
||||
"a/bc",
|
||||
"☁️",
|
||||
"-",
|
||||
"--diff",
|
||||
"-im-here",
|
||||
"a space",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
for _, invalidUsername := range invalidUsernames {
|
||||
t.Logf("Testing username %s", invalidUsername)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": invalidUsername,
|
||||
"email": "user2@example.com",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
data := DecodeJSON(t, resp, map[string]any{})
|
||||
assert.Equal(t, []any{"Name"}, data["errorFields"])
|
||||
assert.Contains(t,
|
||||
data["errorMessage"],
|
||||
translation.NewLocale("en-US").TrString("form.username_error", "Name"),
|
||||
)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
|
||||
}
|
||||
}
|
||||
|
||||
func testRenameReservedUsername(t *testing.T) {
|
||||
// ".", "..", ".well-known" are also reserved but invalid as form input.
|
||||
reservedNames := []string{
|
||||
"api",
|
||||
"openapi3.v1.json",
|
||||
"swagger.v1.json",
|
||||
}
|
||||
patternNotAllowedNames := []string{
|
||||
"name.keys",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
locale := translation.NewLocale("en-US")
|
||||
check := func(name, msgKey string) {
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
data := DecodeJSON(t, resp, map[string]any{})
|
||||
assert.Equal(t, locale.TrString(msgKey, name), data["errorMessage"])
|
||||
assert.Equal(t, []any{"Name"}, data["errorFields"])
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
|
||||
}
|
||||
for _, name := range reservedNames {
|
||||
check(name, "user.form.name_reserved")
|
||||
}
|
||||
for _, name := range patternNotAllowedNames {
|
||||
check(name, "user.form.name_pattern_not_allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func testChangeFullName(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
postSettings := func(t *testing.T, values map[string]string, expectedStatus int) map[string]any {
|
||||
t.Helper()
|
||||
resp := session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/settings", values), expectedStatus)
|
||||
return DecodeJSON(t, resp, map[string]any{})
|
||||
}
|
||||
|
||||
t.Run("Change", func(t *testing.T) {
|
||||
postSettings(t, map[string]string{"full_name": "New Name"}, http.StatusOK)
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2", FullName: "New Name"})
|
||||
})
|
||||
|
||||
t.Run("Disabled", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Admin.UserDisabledFeatures, container.SetOf(setting.UserFeatureChangeFullName))()
|
||||
postSettings(t, map[string]string{"location": "Elsewhere"}, http.StatusOK)
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2", FullName: "New Name", Location: "Elsewhere"})
|
||||
|
||||
data := postSettings(t, map[string]string{"full_name": "Other Name"}, http.StatusBadRequest)
|
||||
assert.Equal(t, []any{"FullName"}, data["errorFields"])
|
||||
assert.Equal(t, "Changing full name is disabled.", data["errorMessage"])
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2", FullName: "New Name"})
|
||||
})
|
||||
|
||||
t.Run("Clear", func(t *testing.T) {
|
||||
postSettings(t, map[string]string{"full_name": ""}, http.StatusOK)
|
||||
assert.Empty(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"}).FullName)
|
||||
})
|
||||
}
|
||||
|
||||
func testRenameUsername(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "newUsername",
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.Equal(t, "/user/settings", DecodeJSON(t, resp, map[string]any{})["redirect"])
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "newUsername"})
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
||||
}
|
||||
|
||||
func testUserLocationMapLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "user2",
|
||||
"email": "user@example.com",
|
||||
"language": "en-US",
|
||||
"location": "A/b",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, `a[href="https://example/foo/A%2Fb"]`, true)
|
||||
}
|
||||
@@ -5,7 +5,6 @@ package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
asymkey_model "gitea.dev/models/asymkey"
|
||||
@@ -18,7 +17,6 @@ import (
|
||||
"gitea.dev/modules/setting"
|
||||
api "gitea.dev/modules/structs"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/translation"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -29,14 +27,10 @@ func TestUser(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
t.Run("ViewUser", testViewUser)
|
||||
t.Run("FollowUser", testUserFollow)
|
||||
t.Run("RenameInvalidUsername", testRenameInvalidUsername)
|
||||
t.Run("RenameReservedUsername", testRenameReservedUsername)
|
||||
t.Run("ViewLimitedAndPrivateUserAndRename", testViewLimitedAndPrivateUserAndRename)
|
||||
t.Run("ExportUserGPGKeys", testExportUserGPGKeys)
|
||||
t.Run("GetUserRss", testGetUserRss)
|
||||
t.Run("ListStopWatches", testUserListStopWatches)
|
||||
t.Run("LocationMapLink", testUserLocationMapLink)
|
||||
t.Run("RenameUsername", testRenameUsername)
|
||||
}
|
||||
|
||||
func testViewUser(t *testing.T) {
|
||||
@@ -71,19 +65,6 @@ ssh-rsa AAAA
|
||||
`, resp.Body.String())
|
||||
}
|
||||
|
||||
func testRenameUsername(t *testing.T) {
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "newUsername",
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "newUsername"})
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: "user2"})
|
||||
}
|
||||
|
||||
func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
// user 22 is a limited visibility org
|
||||
org22 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 22})
|
||||
@@ -146,7 +127,7 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
"name": newName,
|
||||
"visibility": "2", // private
|
||||
})
|
||||
session2.MakeRequest(t, req, http.StatusSeeOther)
|
||||
session2.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: newName})
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: oldName})
|
||||
@@ -157,87 +138,6 @@ func testViewLimitedAndPrivateUserAndRename(t *testing.T) {
|
||||
session.MakeRequest(t, req, http.StatusTemporaryRedirect) // login user2 can visit private visibility user via old name
|
||||
}
|
||||
|
||||
func testRenameInvalidUsername(t *testing.T) {
|
||||
invalidUsernames := []string{
|
||||
"%2f*",
|
||||
"%2f.",
|
||||
"%2f..",
|
||||
"%00",
|
||||
"thisHas ASpace",
|
||||
"p<A>tho>lo<gical",
|
||||
".",
|
||||
"..",
|
||||
".well-known",
|
||||
".abc",
|
||||
"abc.",
|
||||
"a..bc",
|
||||
"a...bc",
|
||||
"a.-bc",
|
||||
"a._bc",
|
||||
"a_-bc",
|
||||
"a/bc",
|
||||
"☁️",
|
||||
"-",
|
||||
"--diff",
|
||||
"-im-here",
|
||||
"a space",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
for _, invalidUsername := range invalidUsernames {
|
||||
t.Logf("Testing username %s", invalidUsername)
|
||||
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": invalidUsername,
|
||||
"email": "user2@example.com",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
assert.Contains(t,
|
||||
htmlDoc.doc.Find(".ui.negative.message").Text(),
|
||||
translation.NewLocale("en-US").TrString("form.username_error", "Name"),
|
||||
)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: invalidUsername})
|
||||
}
|
||||
}
|
||||
|
||||
func testRenameReservedUsername(t *testing.T) {
|
||||
// ".", "..", ".well-known" are also reserved but invalid as form input.
|
||||
reservedNames := []string{
|
||||
"api",
|
||||
"openapi3.v1.json",
|
||||
"swagger.v1.json",
|
||||
}
|
||||
patternNotAllowedNames := []string{
|
||||
"name.keys",
|
||||
}
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
locale := translation.NewLocale("en-US")
|
||||
check := func(name, msgKey string) {
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": name,
|
||||
"email": "user2@example.com",
|
||||
"language": "en-US",
|
||||
})
|
||||
resp := session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
req = NewRequest(t, "GET", test.RedirectURL(resp))
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
actualMsg := strings.TrimSpace(htmlDoc.doc.Find(".ui.negative.message").Text())
|
||||
assert.Equal(t, locale.TrString(msgKey, name), actualMsg)
|
||||
unittest.AssertNotExistsBean(t, &user_model.User{Name: name})
|
||||
}
|
||||
for _, name := range reservedNames {
|
||||
check(name, "user.form.name_reserved")
|
||||
}
|
||||
for _, name := range patternNotAllowedNames {
|
||||
check(name, "user.form.name_pattern_not_allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func testExportUserGPGKeys(t *testing.T) {
|
||||
testExportUserGPGKeys := func(t *testing.T, user, expected string) {
|
||||
t.Helper()
|
||||
@@ -370,24 +270,6 @@ func testUserListStopWatches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testUserLocationMapLink(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.UserLocationMapURL, "https://example/foo/")()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
|
||||
"name": "user2",
|
||||
"email": "user@example.com",
|
||||
"language": "en-US",
|
||||
"location": "A/b",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
req = NewRequest(t, "GET", "/user2/")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
AssertHTMLElement(t, htmlDoc, `a[href="https://example/foo/A%2Fb"]`, true)
|
||||
}
|
||||
|
||||
func testUserFollow(t *testing.T) {
|
||||
bot := &user_model.User{
|
||||
Name: "follow-bot",
|
||||
|
||||
Reference in new issue
Block a user