aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBruno Sofiato <bruno.sofiato@gmail.com>2025-01-31 21:59:49 -0300
committerGitHub <noreply@github.com>2025-02-01 00:59:49 +0000
commit040c830dec5c727a56d16df62b1673bce6fca645 (patch)
treec83ba4db456cf7285d5097bbb28d29fde95e5bd1
parent5b83203f377994cc63baa3f445e26bf85b7e370d (diff)
downloadgitea-040c830dec5c727a56d16df62b1673bce6fca645.tar.gz
gitea-040c830dec5c727a56d16df62b1673bce6fca645.zip
Inclusion of rename organization api (#33303)
This adds an endpoint (`/orgs/{org}/rename`) to rename organizations. I've modeled the endpoint using the rename user endpoint -- `/admin/users/{username}/rename` -- as base. It is the 1st time I wrote a new API endpoint (I've tried to follow the rename users endpoint code while writing it). So feel free to ping me if there is something wrong or missing. Resolves #32995 --------- Signed-off-by: Bruno Sofiato <bruno.sofiato@gmail.com> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
-rw-r--r--modules/structs/org.go9
-rw-r--r--routers/api/v1/admin/user.go18
-rw-r--r--routers/api/v1/api.go1
-rw-r--r--routers/api/v1/org/org.go38
-rw-r--r--routers/api/v1/swagger/options.go3
-rw-r--r--templates/swagger/v1_json.tmpl56
-rw-r--r--tests/integration/api_org_test.go276
7 files changed, 250 insertions, 151 deletions
diff --git a/modules/structs/org.go b/modules/structs/org.go
index c0a545ac1c..f93b3b6493 100644
--- a/modules/structs/org.go
+++ b/modules/structs/org.go
@@ -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"`
+}
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index 21cb2f9ccd..53eee72631 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -477,26 +477,16 @@ func RenameUser(ctx *context.APIContext) {
return
}
- oldName := ctx.ContextUser.Name
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
- // Check if user name has been changed
+ // 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)
}
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index b1a42a85e6..438db4ae71 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -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() {
diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go
index d65f922434..2fcba0bf1a 100644
--- a/routers/api/v1/org/org.go
+++ b/routers/api/v1/org/org.go
@@ -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
diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go
index 125605d98f..353d6de89b 100644
--- a/routers/api/v1/swagger/options.go
+++ b/routers/api/v1/swagger/options.go
@@ -209,5 +209,8 @@ type swaggerParameterBodies struct {
CreateVariableOption api.CreateVariableOption
// in:body
+ RenameOrgOption api.RenameOrgOption
+
+ // in:body
UpdateVariableOption api.UpdateVariableOption
}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index 8082fc594a..c58b21062d 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -2991,6 +2991,46 @@
}
}
},
+ "/orgs/{org}/rename": {
+ "post": {
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "organization"
+ ],
+ "summary": "Rename an organization",
+ "operationId": "renameOrg",
+ "parameters": [
+ {
+ "type": "string",
+ "description": "existing org name",
+ "name": "org",
+ "in": "path",
+ "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"
+ }
+ }
+ }
+ },
"/orgs/{org}/repos": {
"get": {
"produces": [
@@ -24207,6 +24247,22 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
+ "RenameOrgOption": {
+ "description": "RenameOrgOption options when renaming an organization",
+ "type": "object",
+ "required": [
+ "new_name"
+ ],
+ "properties": {
+ "new_name": {
+ "description": "New username for this org. This name cannot be in use yet by any other user.",
+ "type": "string",
+ "uniqueItems": true,
+ "x-go-name": "NewName"
+ }
+ },
+ "x-go-package": "code.gitea.io/gitea/modules/structs"
+ },
"RenameUserOption": {
"description": "RenameUserOption options when renaming a user",
"type": "object",
diff --git a/tests/integration/api_org_test.go b/tests/integration/api_org_test.go
index fff121490c..d766b1e8be 100644
--- a/tests/integration/api_org_test.go
+++ b/tests/integration/api_org_test.go
@@ -6,7 +6,6 @@ package integration
import (
"fmt"
"net/http"
- "net/url"
"strings"
"testing"
@@ -19,46 +18,52 @@ 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) {
- token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
-
- org := api.CreateOrgOption{
- UserName: "user1_org",
- FullName: "User1's organization",
- Description: "This organization created by user1",
- Website: "https://try.gitea.io",
- Location: "Shanghai",
- Visibility: "limited",
- }
- req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).
- AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusCreated)
-
- var apiOrg api.Organization
- DecodeJSON(t, resp, &apiOrg)
-
- assert.Equal(t, org.UserName, apiOrg.Name)
- assert.Equal(t, org.FullName, apiOrg.FullName)
- assert.Equal(t, org.Description, apiOrg.Description)
- assert.Equal(t, org.Website, apiOrg.Website)
- assert.Equal(t, org.Location, apiOrg.Location)
- assert.Equal(t, org.Visibility, apiOrg.Visibility)
-
- unittest.AssertExistsAndLoadBean(t, &user_model.User{
- Name: org.UserName,
- LowerName: strings.ToLower(org.UserName),
- FullName: org.FullName,
- })
+func TestAPIOrgCreateRename(t *testing.T) {
+ defer tests.PrepareTestEnv(t)()
+ token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
+
+ org := api.CreateOrgOption{
+ UserName: "user1_org",
+ FullName: "User1's organization",
+ Description: "This organization created by user1",
+ Website: "https://try.gitea.io",
+ Location: "Shanghai",
+ Visibility: "limited",
+ }
+ req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &org).AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, org.UserName, apiOrg.Name)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ assert.Equal(t, org.Website, apiOrg.Website)
+ assert.Equal(t, org.Location, apiOrg.Location)
+ assert.Equal(t, org.Visibility, apiOrg.Visibility)
+
+ unittest.AssertExistsAndLoadBean(t, &user_model.User{
+ Name: org.UserName,
+ LowerName: strings.ToLower(org.UserName),
+ 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 {
@@ -71,103 +76,101 @@ 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) {
- session := loginUser(t, "user1")
-
- token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
- org := api.EditOrgOption{
- FullName: "Org3 organization new full name",
- Description: "A new description",
- Website: "https://try.gitea.io/new",
- Location: "Beijing",
- Visibility: "private",
- }
- req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
- AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusOK)
-
- var apiOrg api.Organization
- DecodeJSON(t, resp, &apiOrg)
-
- assert.Equal(t, "org3", apiOrg.Name)
- assert.Equal(t, org.FullName, apiOrg.FullName)
- assert.Equal(t, org.Description, apiOrg.Description)
- assert.Equal(t, org.Website, apiOrg.Website)
- assert.Equal(t, org.Location, apiOrg.Location)
- assert.Equal(t, org.Visibility, apiOrg.Visibility)
- })
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
+ org := api.EditOrgOption{
+ FullName: "Org3 organization new full name",
+ Description: "A new description",
+ Website: "https://try.gitea.io/new",
+ Location: "Beijing",
+ Visibility: "private",
+ }
+ req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiOrg api.Organization
+ DecodeJSON(t, resp, &apiOrg)
+
+ assert.Equal(t, "org3", apiOrg.Name)
+ assert.Equal(t, org.FullName, apiOrg.FullName)
+ assert.Equal(t, org.Description, apiOrg.Description)
+ 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) {
- session := loginUser(t, "user1")
-
- token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
- org := api.EditOrgOption{
- FullName: "Org3 organization new full name",
- Description: "A new description",
- Website: "https://try.gitea.io/new",
- Location: "Beijing",
- Visibility: "badvisibility",
- }
- req := NewRequestWithJSON(t, "PATCH", "/api/v1/orgs/org3", &org).
- AddTokenAuth(token)
- MakeRequest(t, req, http.StatusUnprocessableEntity)
- })
+ defer tests.PrepareTestEnv(t)()
+ session := loginUser(t, "user1")
+
+ token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
+ org := api.EditOrgOption{
+ FullName: "Org3 organization new full name",
+ Description: "A new description",
+ Website: "https://try.gitea.io/new",
+ Location: "Beijing",
+ Visibility: "badvisibility",
+ }
+ 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)
- MakeRequest(t, req, http.StatusNotFound)
+ orgName := "user1_org"
+ req := NewRequestf(t, "GET", "/api/v1/orgs/%s", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
- MakeRequest(t, req, http.StatusNotFound)
+ req = NewRequestf(t, "GET", "/api/v1/orgs/%s/repos", orgName)
+ MakeRequest(t, req, http.StatusNotFound)
- req = NewRequestf(t, "GET", "/api/v1/orgs/%s/members", orgName)
- MakeRequest(t, req, http.StatusNotFound)
- })
+ 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
@@ -192,37 +195,36 @@ func TestAPIGetAll(t *testing.T) {
}
func TestAPIOrgSearchEmptyTeam(t *testing.T) {
- onGiteaRun(t, func(*testing.T, *url.URL) {
- token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
- orgName := "org_with_empty_team"
-
- // create org
- req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
- UserName: orgName,
- }).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusCreated)
-
- // create team with no member
- req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
- Name: "Empty",
- IncludesAllRepositories: true,
- Permission: "read",
- Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
- }).AddTokenAuth(token)
- MakeRequest(t, req, http.StatusCreated)
-
- // case-insensitive search for teams that have no members
- req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
- AddTokenAuth(token)
- resp := MakeRequest(t, req, http.StatusOK)
- data := struct {
- Ok bool
- Data []*api.Team
- }{}
- DecodeJSON(t, resp, &data)
- assert.True(t, data.Ok)
- if assert.Len(t, data.Data, 1) {
- assert.EqualValues(t, "Empty", data.Data[0].Name)
- }
- })
+ defer tests.PrepareTestEnv(t)()
+ token := getUserToken(t, "user1", auth_model.AccessTokenScopeWriteOrganization)
+ orgName := "org_with_empty_team"
+
+ // create org
+ req := NewRequestWithJSON(t, "POST", "/api/v1/orgs", &api.CreateOrgOption{
+ UserName: orgName,
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // create team with no member
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/orgs/%s/teams", orgName), &api.CreateTeamOption{
+ Name: "Empty",
+ IncludesAllRepositories: true,
+ Permission: "read",
+ Units: []string{"repo.code", "repo.issues", "repo.ext_issues", "repo.wiki", "repo.pulls"},
+ }).AddTokenAuth(token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ // case-insensitive search for teams that have no members
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/orgs/%s/teams/search?q=%s", orgName, "empty")).
+ AddTokenAuth(token)
+ resp := MakeRequest(t, req, http.StatusOK)
+ data := struct {
+ Ok bool
+ Data []*api.Team
+ }{}
+ DecodeJSON(t, resp, &data)
+ assert.True(t, data.Ok)
+ if assert.Len(t, data.Data, 1) {
+ assert.EqualValues(t, "Empty", data.Data[0].Name)
+ }
}