Skip to content

Inclusion of rename organization api #33303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/structs/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,12 @@ type EditOrgOption struct {
Visibility string `json:"visibility" binding:"In(,public,limited,private)"`
RepoAdminChangeTeamAccess *bool `json:"repo_admin_change_team_access"`
}

// RenameOrgOption options when renaming an organization
type RenameOrgOption struct {
// New username for this org. This name cannot be in use yet by any other user.
//
// required: true
// unique: true
NewName string `json:"new_name" binding:"Required"`
}
16 changes: 3 additions & 13 deletions routers/api/v1/admin/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,26 +477,16 @@ func RenameUser(ctx *context.APIContext) {
return
}

oldName := ctx.ContextUser.Name
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName

// Check if username has been changed
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
switch {
case user_model.IsErrUserAlreadyExist(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
case db.IsErrNameReserved(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
case db.IsErrNamePatternNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
case db.IsErrNameCharsNotAllowed(err):
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
default:
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "", err)
} else {
ctx.ServerError("ChangeUserName", err)
}
return
}

log.Trace("User name changed: %s -> %s", oldName, newName)
ctx.Status(http.StatusNoContent)
}
1 change: 1 addition & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1530,6 +1530,7 @@ func Routes() *web.Router {
m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
m.Post("/rename", reqToken(), reqOrgOwnership(), bind(api.RenameOrgOption{}), org.Rename)
m.Combo("/repos").Get(user.ListOrgRepos).
Post(reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepo)
m.Group("/members", func() {
Expand Down
38 changes: 38 additions & 0 deletions routers/api/v1/org/org.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,44 @@ func Get(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, org)
}

func Rename(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/rename organization renameOrg
// ---
// summary: Rename an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: existing org name
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/RenameOrgOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "422":
// "$ref": "#/responses/validationError"

form := web.GetForm(ctx).(*api.RenameOrgOption)
orgUser := ctx.Org.Organization.AsUser()
if err := user_service.RenameUser(ctx, orgUser, form.NewName); err != nil {
if user_model.IsErrUserAlreadyExist(err) || db.IsErrNameReserved(err) || db.IsErrNamePatternNotAllowed(err) || db.IsErrNameCharsNotAllowed(err) {
ctx.Error(http.StatusUnprocessableEntity, "RenameOrg", err)
} else {
ctx.ServerError("RenameOrg", err)
}
return
}
ctx.Status(http.StatusNoContent)
}

// Edit change an organization's information
func Edit(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org} organization orgEdit
Expand Down
3 changes: 3 additions & 0 deletions routers/api/v1/swagger/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ type swaggerParameterBodies struct {
// in:body
CreateVariableOption api.CreateVariableOption

// in:body
RenameOrgOption api.RenameOrgOption

// in:body
UpdateVariableOption api.UpdateVariableOption
}
56 changes: 56 additions & 0 deletions templates/swagger/v1_json.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 38 additions & 36 deletions tests/integration/api_org_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package integration
import (
"fmt"
"net/http"
"net/url"
"strings"
"testing"

Expand All @@ -19,13 +18,14 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/test"
"code.gitea.io/gitea/tests"

"github.com/stretchr/testify/assert"
)

func TestAPIOrgCreate(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
func TestAPIOrgCreateRename(t *testing.T) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)

org := api.CreateOrgOption{
Expand All @@ -36,8 +36,7 @@ func TestAPIOrgCreate(t *testing.T) {
Location: "Shanghai",
Visibility: "limited",
}
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).
AddTokenAuth(token)
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)

var apiOrg api.Organization
Expand All @@ -56,9 +55,15 @@ func TestAPIOrgCreate(t *testing.T) {
FullName: org.FullName,
})

// check org name
req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiOrg)
assert.EqualValues(t, org.UserName, apiOrg.Name)

t.Run("CheckPermission", func(t *testing.T) {
// Check owner team permission
ownerTeam, _ := org_model.GetOwnerTeam(db.DefaultContext, apiOrg.ID)

for _, ut := range unit_model.AllRepoUnitTypes {
up := perm.AccessModeOwner
if ut == unit_model.TypeExternalTracker || ut == unit_model.TypeExternalWiki {
Expand All @@ -71,37 +76,42 @@ func TestAPIOrgCreate(t *testing.T) {
AccessMode: up,
})
}
})

req = NewRequestf(t, "GET", "/api/v1/orgs/%s", org.UserName).
AddTokenAuth(token)
t.Run("CheckMembers", func(t *testing.T) {
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
DecodeJSON(t, resp, &apiOrg)
assert.EqualValues(t, org.UserName, apiOrg.Name)

req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
// user1 on this org is public
var users []*api.User
DecodeJSON(t, resp, &users)
assert.Len(t, users, 1)
assert.EqualValues(t, "user1", users[0].UserName)
})

t.Run("RenameOrg", func(t *testing.T) {
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user1_org/rename", &api.RenameOrgOption{
NewName: "renamed_org",
}).AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
unittest.AssertExistsAndLoadBean(t, &org_model.Organization{Name: "renamed_org"})
org.UserName = "renamed_org" // update the variable so the following tests could still use it
})

t.Run("ListRepos", func(t *testing.T) {
// FIXME: this test is wrong, there is no repository at all, so the for-loop is empty
req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", org.UserName).AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
var repos []*api.Repository
DecodeJSON(t, resp, &repos)
for _, repo := range repos {
assert.False(t, repo.Private)
}

req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", org.UserName).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)

// user1 on this org is public
var users []*api.User
DecodeJSON(t, resp, &users)
assert.Len(t, users, 1)
assert.EqualValues(t, "user1", users[0].UserName)
})
}

func TestAPIOrgEdit(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")

token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
Expand All @@ -125,11 +135,10 @@ func TestAPIOrgEdit(t *testing.T) {
assert.Equal(t, org.Website, apiOrg.Website)
assert.Equal(t, org.Location, apiOrg.Location)
assert.Equal(t, org.Visibility, apiOrg.Visibility)
})
}

func TestAPIOrgEditBadVisibility(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")

token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
Expand All @@ -143,15 +152,11 @@ func TestAPIOrgEditBadVisibility(t *testing.T) {
req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusUnprocessableEntity)
})
}

func TestAPIOrgDeny(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
setting.Service.RequireSignInView = true
defer func() {
setting.Service.RequireSignInView = false
}()
defer tests.PrepareTestEnv(t)()
defer test.MockVariableValue(&setting.Service.RequireSignInView, true)()

orgName := "user1_org"
req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
Expand All @@ -162,12 +167,10 @@ func TestAPIOrgDeny(t *testing.T) {

req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
MakeRequest(t, req, http.StatusNotFound)
})
}

func TestAPIGetAll(t *testing.T) {
defer tests.PrepareTestEnv(t)()

token := getUserToken(t, "user1", auth_model.AccessTokenScopeReadOrganization)

// accessing with a token will return all orgs
Expand All @@ -192,7 +195,7 @@ func TestAPIGetAll(t *testing.T) {
}

func TestAPIOrgSearchEmptyTeam(t *testing.T) {
onGiteaRun(t, func(*testing.T, *url.URL) {
defer tests.PrepareTestEnv(t)()
token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
orgName := "org_with_empty_team"

Expand Down Expand Up @@ -224,5 +227,4 @@ func TestAPIOrgSearchEmptyTeam(t *testing.T) {
if assert.Len(t, data.Data, 1) {
assert.EqualValues(t, "Empty", data.Data[0].Name)
}
})
}