mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-18 07:48:48 +01:00
Fix org contact email not clearable once set (#36975)
When the email field was submitted as empty in org settings (web and API), the previous guard `if form.Email != ""` silently skipped the update, making it impossible to remove a contact email after it was set. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
@@ -66,23 +66,21 @@ type CreateOrgOption struct {
|
|||||||
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
|
RepoAdminChangeTeamAccess bool `json:"repo_admin_change_team_access"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make EditOrgOption fields optional after https://gitea.com/go-chi/binding/pulls/5 got merged
|
|
||||||
|
|
||||||
// EditOrgOption options for editing an organization
|
// EditOrgOption options for editing an organization
|
||||||
type EditOrgOption struct {
|
type EditOrgOption struct {
|
||||||
// The full display name of the organization
|
// The full display name of the organization
|
||||||
FullName string `json:"full_name" binding:"MaxSize(100)"`
|
FullName *string `json:"full_name" binding:"MaxSize(100)"`
|
||||||
// The email address of the organization
|
// The email address of the organization; use empty string to clear
|
||||||
Email string `json:"email" binding:"MaxSize(255)"`
|
Email *string `json:"email" binding:"MaxSize(255)"`
|
||||||
// The description of the organization
|
// The description of the organization
|
||||||
Description string `json:"description" binding:"MaxSize(255)"`
|
Description *string `json:"description" binding:"MaxSize(255)"`
|
||||||
// The website URL of the organization
|
// The website URL of the organization
|
||||||
Website string `json:"website" binding:"ValidUrl;MaxSize(255)"`
|
Website *string `json:"website" binding:"ValidUrl;MaxSize(255)"`
|
||||||
// The location of the organization
|
// The location of the organization
|
||||||
Location string `json:"location" binding:"MaxSize(50)"`
|
Location *string `json:"location" binding:"MaxSize(50)"`
|
||||||
// possible values are `public`, `limited` or `private`
|
// possible values are `public`, `limited` or `private`
|
||||||
// enum: public,limited,private
|
// enum: public,limited,private
|
||||||
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
|
Visibility *string `json:"visibility" binding:"In(,public,limited,private)"`
|
||||||
// Whether repository administrators can change team access
|
// Whether repository administrators can change team access
|
||||||
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
|
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package org
|
package org
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
activities_model "code.gitea.io/gitea/models/activities"
|
activities_model "code.gitea.io/gitea/models/activities"
|
||||||
@@ -14,6 +15,7 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
"code.gitea.io/gitea/modules/web"
|
"code.gitea.io/gitea/modules/web"
|
||||||
"code.gitea.io/gitea/routers/api/v1/user"
|
"code.gitea.io/gitea/routers/api/v1/user"
|
||||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||||
@@ -379,19 +381,21 @@ func Edit(ctx *context.APIContext) {
|
|||||||
|
|
||||||
form := web.GetForm(ctx).(*api.EditOrgOption)
|
form := web.GetForm(ctx).(*api.EditOrgOption)
|
||||||
|
|
||||||
if form.Email != "" {
|
if err := org.UpdateOrgEmailAddress(ctx, ctx.Org.Organization, form.Email); err != nil {
|
||||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
|
if errors.Is(err, util.ErrInvalidArgument) {
|
||||||
ctx.APIErrorInternal(err)
|
ctx.APIError(http.StatusUnprocessableEntity, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.APIErrorInternal(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &user_service.UpdateOptions{
|
opts := &user_service.UpdateOptions{
|
||||||
FullName: optional.Some(form.FullName),
|
FullName: optional.FromPtr(form.FullName),
|
||||||
Description: optional.Some(form.Description),
|
Description: optional.FromPtr(form.Description),
|
||||||
Website: optional.Some(form.Website),
|
Website: optional.FromPtr(form.Website),
|
||||||
Location: optional.Some(form.Location),
|
Location: optional.FromPtr(form.Location),
|
||||||
Visibility: optional.FromMapLookup(api.VisibilityModes, form.Visibility),
|
Visibility: optional.FromMapLookup(api.VisibilityModes, optional.FromPtr(form.Visibility).Value()),
|
||||||
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||||
}
|
}
|
||||||
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
|
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
package org
|
package org
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
@@ -69,24 +70,25 @@ func SettingsPost(ctx *context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
org := ctx.Org.Organization
|
org := ctx.Org.Organization
|
||||||
|
if err := org_service.UpdateOrgEmailAddress(ctx, org, form.Email); err != nil {
|
||||||
if form.Email != "" {
|
if errors.Is(err, util.ErrInvalidArgument) {
|
||||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, org.AsUser(), form.Email); err != nil {
|
|
||||||
ctx.Data["Err_Email"] = true
|
ctx.Data["Err_Email"] = true
|
||||||
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.email_invalid"), tplSettingsOptions, &form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
ctx.ServerError("UpdateOrgEmailAddress", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &user_service.UpdateOptions{
|
opts := &user_service.UpdateOptions{
|
||||||
FullName: optional.Some(form.FullName),
|
FullName: optional.FromPtr(form.FullName),
|
||||||
Description: optional.Some(form.Description),
|
Description: optional.FromPtr(form.Description),
|
||||||
Website: optional.Some(form.Website),
|
Website: optional.FromPtr(form.Website),
|
||||||
Location: optional.Some(form.Location),
|
Location: optional.FromPtr(form.Location),
|
||||||
RepoAdminChangeTeamAccess: optional.Some(form.RepoAdminChangeTeamAccess),
|
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||||
}
|
}
|
||||||
if ctx.Doer.IsAdmin {
|
if ctx.Doer.IsAdmin {
|
||||||
opts.MaxRepoCreation = optional.Some(form.MaxRepoCreation)
|
opts.MaxRepoCreation = optional.FromPtr(form.MaxRepoCreation)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
if err := user_service.UpdateUser(ctx, org.AsUser(), opts); err != nil {
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ func (f *CreateOrgForm) Validate(req *http.Request, errs binding.Errors) binding
|
|||||||
|
|
||||||
// UpdateOrgSettingForm form for updating organization settings
|
// UpdateOrgSettingForm form for updating organization settings
|
||||||
type UpdateOrgSettingForm struct {
|
type UpdateOrgSettingForm struct {
|
||||||
FullName string `binding:"MaxSize(100)"`
|
FullName *string `binding:"MaxSize(100)"`
|
||||||
Email string `binding:"MaxSize(255)"`
|
Email *string `binding:"MaxSize(255)"`
|
||||||
Description string `binding:"MaxSize(255)"`
|
Description *string `binding:"MaxSize(255)"`
|
||||||
Website string `binding:"ValidUrl;MaxSize(255)"`
|
Website *string `binding:"ValidUrl;MaxSize(255)"`
|
||||||
Location string `binding:"MaxSize(50)"`
|
Location *string `binding:"MaxSize(50)"`
|
||||||
MaxRepoCreation int
|
MaxRepoCreation *int
|
||||||
RepoAdminChangeTeamAccess bool
|
RepoAdminChangeTeamAccess *bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|||||||
@@ -168,3 +168,20 @@ func ChangeOrganizationVisibility(ctx context.Context, org *org_model.Organizati
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateOrgEmailAddress validates and updates the organization's contact email.
|
||||||
|
// A nil email means no change.
|
||||||
|
func UpdateOrgEmailAddress(ctx context.Context, org *org_model.Organization, email *string) error {
|
||||||
|
if email == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if *email != "" {
|
||||||
|
if err := user_model.ValidateEmail(*email); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
org.Email = *email
|
||||||
|
return user_model.UpdateUserCols(ctx, org.AsUser(), "email")
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,28 +10,54 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
unittest.MainTest(m)
|
unittest.MainTest(m)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeleteOrganization(t *testing.T) {
|
func TestOrg(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
require.NoError(t, unittest.PrepareTestDatabase())
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
|
|
||||||
assert.NoError(t, DeleteOrganization(t.Context(), org, false))
|
|
||||||
unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
|
|
||||||
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
|
|
||||||
unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
|
|
||||||
|
|
||||||
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
t.Run("UpdateOrgEmailAddress", func(t *testing.T) {
|
||||||
err := DeleteOrganization(t.Context(), org, false)
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||||
assert.Error(t, err)
|
originalEmail := org.Email
|
||||||
assert.True(t, repo_model.IsErrUserOwnRepos(err))
|
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
|
require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, nil))
|
||||||
assert.Error(t, DeleteOrganization(t.Context(), user, false))
|
unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: originalEmail})
|
||||||
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
|
||||||
|
newEmail := "contact@org3.example.com"
|
||||||
|
require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, &newEmail))
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail})
|
||||||
|
|
||||||
|
invalidEmail := "invalid email"
|
||||||
|
err := UpdateOrgEmailAddress(t.Context(), org, &invalidEmail)
|
||||||
|
require.ErrorIs(t, err, util.ErrInvalidArgument)
|
||||||
|
unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: newEmail})
|
||||||
|
|
||||||
|
require.NoError(t, UpdateOrgEmailAddress(t.Context(), org, new("")))
|
||||||
|
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3, Email: ""})
|
||||||
|
assert.Empty(t, org.Email)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteOrganization", func(t *testing.T) {
|
||||||
|
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
|
||||||
|
assert.NoError(t, DeleteOrganization(t.Context(), org, false))
|
||||||
|
unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
|
||||||
|
unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
|
||||||
|
unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
|
||||||
|
|
||||||
|
org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||||
|
err := DeleteOrganization(t.Context(), org, false)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, repo_model.IsErrUserOwnRepos(err))
|
||||||
|
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
|
||||||
|
assert.Error(t, DeleteOrganization(t.Context(), user, false))
|
||||||
|
unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ReplacePrimaryEmailAddress replaces the user's primary email address with the given email address.
|
||||||
|
// It also updates the user's email field to match the new primary email address.
|
||||||
func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
|
func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailStr string) error {
|
||||||
|
// FIXME: this check is from old logic, but it is not right, there are far more user types, not only "organization"
|
||||||
|
if u.IsOrganization() {
|
||||||
|
return util.NewInvalidArgumentErrorf("user %s is an organization", u.Name)
|
||||||
|
}
|
||||||
|
|
||||||
if strings.EqualFold(u.Email, emailStr) {
|
if strings.EqualFold(u.Email, emailStr) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -24,41 +31,38 @@ func ReplacePrimaryEmailAddress(ctx context.Context, u *user_model.User, emailSt
|
|||||||
}
|
}
|
||||||
|
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if !u.IsOrganization() {
|
// Check if address exists already
|
||||||
// Check if address exists already
|
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
|
||||||
email, err := user_model.GetEmailAddressByEmail(ctx, emailStr)
|
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
if email != nil {
|
||||||
if email != nil {
|
if email.IsPrimary && email.UID == u.ID {
|
||||||
if email.IsPrimary && email.UID == u.ID {
|
return nil
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
|
|
||||||
}
|
}
|
||||||
|
return user_model.ErrEmailAlreadyUsed{Email: emailStr}
|
||||||
|
}
|
||||||
|
|
||||||
// Remove old primary address
|
// Remove old primary address
|
||||||
primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
|
primary, err := user_model.GetPrimaryEmailAddressOfUser(ctx, u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil {
|
if _, err := db.DeleteByID[user_model.EmailAddress](ctx, primary.ID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert new primary address
|
// Insert new primary address
|
||||||
if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{
|
if _, err := user_model.InsertEmailAddress(ctx, &user_model.EmailAddress{
|
||||||
UID: u.ID,
|
UID: u.ID,
|
||||||
Email: emailStr,
|
Email: emailStr,
|
||||||
IsActivated: true,
|
IsActivated: true,
|
||||||
IsPrimary: true,
|
IsPrimary: true,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u.Email = emailStr
|
u.Email = emailStr
|
||||||
|
|
||||||
return user_model.UpdateUserCols(ctx, u, "email")
|
return user_model.UpdateUserCols(ctx, u, "email")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,16 @@ package user
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
organization_model "code.gitea.io/gitea/models/organization"
|
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReplacePrimaryEmailAddress(t *testing.T) {
|
func TestUserEmail(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
t.Run("User", func(t *testing.T) {
|
t.Run("PrimaryEmailAddress", func(t *testing.T) {
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13})
|
||||||
|
|
||||||
emails, err := user_model.GetEmailAddresses(t.Context(), user.ID)
|
emails, err := user_model.GetEmailAddresses(t.Context(), user.ID)
|
||||||
@@ -42,50 +41,36 @@ func TestReplacePrimaryEmailAddress(t *testing.T) {
|
|||||||
assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), user, "primary-13@example.com"))
|
assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), user, "primary-13@example.com"))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Organization", func(t *testing.T) {
|
t.Run("AddEmailAddresses", func(t *testing.T) {
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &organization_model.Organization{ID: 3})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
assert.Equal(t, "org3@example.com", org.Email)
|
assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "}))
|
||||||
|
|
||||||
assert.NoError(t, ReplacePrimaryEmailAddress(t.Context(), org.AsUser(), "primary-org@example.com"))
|
emails := []string{"user1234@example.com", "user5678@example.com"}
|
||||||
|
|
||||||
assert.Equal(t, "primary-org@example.com", org.Email)
|
assert.NoError(t, AddEmailAddresses(t.Context(), user, emails))
|
||||||
|
|
||||||
|
err := AddEmailAddresses(t.Context(), user, emails)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("DeleteEmailAddresses", func(t *testing.T) {
|
||||||
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||||
|
|
||||||
|
emails := []string{"user2-2@example.com"}
|
||||||
|
|
||||||
|
err := DeleteEmailAddresses(t.Context(), user, emails)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = DeleteEmailAddresses(t.Context(), user, emails)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
|
||||||
|
|
||||||
|
emails = []string{"user2@example.com"}
|
||||||
|
|
||||||
|
err = DeleteEmailAddresses(t.Context(), user, emails)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAddEmailAddresses(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
||||||
|
|
||||||
assert.Error(t, AddEmailAddresses(t.Context(), user, []string{" invalid email "}))
|
|
||||||
|
|
||||||
emails := []string{"user1234@example.com", "user5678@example.com"}
|
|
||||||
|
|
||||||
assert.NoError(t, AddEmailAddresses(t.Context(), user, emails))
|
|
||||||
|
|
||||||
err := AddEmailAddresses(t.Context(), user, emails)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, user_model.IsErrEmailAlreadyUsed(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeleteEmailAddresses(t *testing.T) {
|
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
||||||
|
|
||||||
emails := []string{"user2-2@example.com"}
|
|
||||||
|
|
||||||
err := DeleteEmailAddresses(t.Context(), user, emails)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = DeleteEmailAddresses(t.Context(), user, emails)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, user_model.IsErrEmailAddressNotExist(err))
|
|
||||||
|
|
||||||
emails = []string{"user2@example.com"}
|
|
||||||
|
|
||||||
err = DeleteEmailAddresses(t.Context(), user, emails)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, user_model.IsErrPrimaryEmailCannotDelete(err))
|
|
||||||
}
|
|
||||||
|
|||||||
2
templates/swagger/v1_json.tmpl
generated
2
templates/swagger/v1_json.tmpl
generated
@@ -24855,7 +24855,7 @@
|
|||||||
"x-go-name": "Description"
|
"x-go-name": "Description"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"description": "The email address of the organization",
|
"description": "The email address of the organization; use empty string to clear",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"x-go-name": "Email"
|
"x-go-name": "Email"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -137,34 +137,45 @@ func TestAPIOrgGeneral(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("OrgEdit", func(t *testing.T) {
|
t.Run("OrgEdit", func(t *testing.T) {
|
||||||
org := api.EditOrgOption{
|
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
|
||||||
FullName: "Org3 organization new full name",
|
assert.NotEqual(t, api.VisibleTypeLimited, org3.Visibility)
|
||||||
Description: "A new description",
|
|
||||||
Website: "https://try.gitea.io/new",
|
|
||||||
Location: "Beijing",
|
|
||||||
Visibility: "private",
|
|
||||||
}
|
|
||||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
|
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
|
||||||
|
|
||||||
var apiOrg api.Organization
|
org3Edit := api.EditOrgOption{
|
||||||
DecodeJSON(t, resp, &apiOrg)
|
FullName: new("new full name"),
|
||||||
|
Description: new("new description"),
|
||||||
|
Website: new("https://org3-new-website.example.com"),
|
||||||
|
Location: new("new location"),
|
||||||
|
Visibility: new("limited"),
|
||||||
|
Email: new("org3-new-email@example.com"),
|
||||||
|
}
|
||||||
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org3Edit).AddTokenAuth(user1Token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
apiOrg := DecodeJSON(t, resp, &api.Organization{})
|
||||||
|
|
||||||
assert.Equal(t, "org3", apiOrg.Name)
|
assert.Equal(t, "org3", apiOrg.Name)
|
||||||
assert.Equal(t, org.FullName, apiOrg.FullName)
|
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
|
||||||
assert.Equal(t, org.Description, apiOrg.Description)
|
assert.Equal(t, *org3Edit.Description, apiOrg.Description)
|
||||||
assert.Equal(t, org.Website, apiOrg.Website)
|
assert.Equal(t, *org3Edit.Website, apiOrg.Website)
|
||||||
assert.Equal(t, org.Location, apiOrg.Location)
|
assert.Equal(t, *org3Edit.Location, apiOrg.Location)
|
||||||
assert.Equal(t, org.Visibility, apiOrg.Visibility)
|
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
|
||||||
|
assert.Equal(t, *org3Edit.Email, apiOrg.Email)
|
||||||
|
org3 = unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "org3"})
|
||||||
|
assert.Equal(t, api.VisibleTypeLimited, org3.Visibility)
|
||||||
|
|
||||||
|
// empty email can clear the email, nil fields won't change the settings
|
||||||
|
req = NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &api.EditOrgOption{
|
||||||
|
Email: new(""),
|
||||||
|
}).AddTokenAuth(user1Token)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
apiOrg = DecodeJSON(t, resp, &api.Organization{})
|
||||||
|
assert.Equal(t, *org3Edit.FullName, apiOrg.FullName)
|
||||||
|
assert.Equal(t, *org3Edit.Visibility, apiOrg.Visibility)
|
||||||
|
assert.Empty(t, apiOrg.Email)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("OrgEditBadVisibility", func(t *testing.T) {
|
t.Run("OrgEditInvalidVisibility", func(t *testing.T) {
|
||||||
org := api.EditOrgOption{
|
org := api.EditOrgOption{
|
||||||
FullName: "Org3 organization new full name",
|
Visibility: new("invalid-visibility"),
|
||||||
Description: "A new description",
|
|
||||||
Website: "https://try.gitea.io/new",
|
|
||||||
Location: "Beijing",
|
|
||||||
Visibility: "badvisibility",
|
|
||||||
}
|
}
|
||||||
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
|
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).AddTokenAuth(user1Token)
|
||||||
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
MakeRequest(t, req, http.StatusUnprocessableEntity)
|
||||||
|
|||||||
@@ -410,12 +410,13 @@ func logUnexpectedResponse(t testing.TB, recorder *httptest.ResponseRecorder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecodeJSON(t testing.TB, resp *httptest.ResponseRecorder, v any) {
|
func DecodeJSON[T any](t testing.TB, resp *httptest.ResponseRecorder, v T) (ret T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
|
// FIXME: JSON-KEY-CASE: for testing purpose only, because many structs don't provide `json` tags, they just use capitalized field names
|
||||||
decoder := json.NewDecoderCaseInsensitive(resp.Body)
|
decoder := json.NewDecoderCaseInsensitive(resp.Body)
|
||||||
require.NoError(t, decoder.Decode(v))
|
require.NoError(t, decoder.Decode(v))
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {
|
func VerifyJSONSchema(t testing.TB, resp *httptest.ResponseRecorder, schemaFile string) {
|
||||||
|
|||||||
@@ -23,9 +23,18 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOrgRepos(t *testing.T) {
|
func TestOrg(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
t.Run("OrgRepos", testOrgRepos)
|
||||||
|
t.Run("PrivateOrg", testPrivateOrg)
|
||||||
|
t.Run("LimitedOrg", testLimitedOrg)
|
||||||
|
t.Run("OrgMembers", testOrgMembers)
|
||||||
|
t.Run("OrgRestrictedUser", testOrgRestrictedUser)
|
||||||
|
t.Run("TeamSearch", testTeamSearch)
|
||||||
|
t.Run("OrgSettings", testOrgSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOrgRepos(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
users = []string{"user1", "user2"}
|
users = []string{"user1", "user2"}
|
||||||
cases = map[string][]string{
|
cases = map[string][]string{
|
||||||
@@ -53,10 +62,8 @@ func TestOrgRepos(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLimitedOrg(t *testing.T) {
|
func testLimitedOrg(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
// not logged-in user
|
||||||
|
|
||||||
// not logged in user
|
|
||||||
req := NewRequest(t, "GET", "/limited_org")
|
req := NewRequest(t, "GET", "/limited_org")
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
|
req = NewRequest(t, "GET", "/limited_org/public_repo_on_limited_org")
|
||||||
@@ -83,10 +90,8 @@ func TestLimitedOrg(t *testing.T) {
|
|||||||
session.MakeRequest(t, req, http.StatusOK)
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrivateOrg(t *testing.T) {
|
func testPrivateOrg(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
// not logged-in user
|
||||||
|
|
||||||
// not logged in user
|
|
||||||
req := NewRequest(t, "GET", "/privated_org")
|
req := NewRequest(t, "GET", "/privated_org")
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
|
req = NewRequest(t, "GET", "/privated_org/public_repo_on_private_org")
|
||||||
@@ -122,10 +127,8 @@ func TestPrivateOrg(t *testing.T) {
|
|||||||
session.MakeRequest(t, req, http.StatusOK)
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOrgMembers(t *testing.T) {
|
func testOrgMembers(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
// not logged-in user
|
||||||
|
|
||||||
// not logged in user
|
|
||||||
req := NewRequest(t, "GET", "/org/org25/members")
|
req := NewRequest(t, "GET", "/org/org25/members")
|
||||||
MakeRequest(t, req, http.StatusOK)
|
MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
@@ -140,9 +143,7 @@ func TestOrgMembers(t *testing.T) {
|
|||||||
session.MakeRequest(t, req, http.StatusOK)
|
session.MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOrgRestrictedUser(t *testing.T) {
|
func testOrgRestrictedUser(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
// privated_org is a private org who has id 23
|
// privated_org is a private org who has id 23
|
||||||
orgName := "privated_org"
|
orgName := "privated_org"
|
||||||
|
|
||||||
@@ -200,9 +201,7 @@ func TestOrgRestrictedUser(t *testing.T) {
|
|||||||
restrictedSession.MakeRequest(t, req, http.StatusOK)
|
restrictedSession.MakeRequest(t, req, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTeamSearch(t *testing.T) {
|
func testTeamSearch(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
|
||||||
|
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
|
||||||
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
|
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17})
|
||||||
|
|
||||||
@@ -251,3 +250,24 @@ func TestTeamSearch(t *testing.T) {
|
|||||||
assert.Len(t, teams, 1) // team permission is "write", so can write "code"
|
assert.Len(t, teams, 1) // team permission is "write", so can write "code"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testOrgSettings(t *testing.T) {
|
||||||
|
session := loginUser(t, "user2")
|
||||||
|
|
||||||
|
req := NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{
|
||||||
|
"full_name": "org3 new full name",
|
||||||
|
"email": "org3-new-email@example.com",
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
|
assert.Equal(t, "org3 new full name", org.FullName)
|
||||||
|
assert.Equal(t, "org3-new-email@example.com", org.Email)
|
||||||
|
|
||||||
|
req = NewRequestWithValues(t, "POST", "/org/org3/settings", map[string]string{
|
||||||
|
"email": "", // empty email means "clear email"
|
||||||
|
})
|
||||||
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||||
|
org = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3})
|
||||||
|
assert.Equal(t, "org3 new full name", org.FullName)
|
||||||
|
assert.Empty(t, org.Email)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user