[API] List, Check, Add & delete endpoints for repository teams (#13630)
* List, Check, Add & delete endpoints for repository teams * return units on single team responce too * Add Tests
This commit is contained in:
		
							parent
							
								
									a9188631b9
								
							
						
					
					
						commit
						6d27703f14
					
				
					 4 changed files with 483 additions and 0 deletions
				
			
		
							
								
								
									
										77
									
								
								integrations/api_repo_teams_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								integrations/api_repo_teams_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| // Copyright 2021 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package integrations | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestAPIRepoTeams(t *testing.T) { | ||||
| 	defer prepareTestEnv(t)() | ||||
| 
 | ||||
| 	// publicOrgRepo = user3/repo21
 | ||||
| 	publicOrgRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 32}).(*models.Repository) | ||||
| 	// user4
 | ||||
| 	user := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) | ||||
| 	session := loginUser(t, user.Name) | ||||
| 	token := getTokenForLoggedInUser(t, session) | ||||
| 
 | ||||
| 	// ListTeams
 | ||||
| 	url := fmt.Sprintf("/api/v1/repos/%s/teams?token=%s", publicOrgRepo.FullName(), token) | ||||
| 	req := NewRequest(t, "GET", url) | ||||
| 	res := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var teams []*api.Team | ||||
| 	DecodeJSON(t, res, &teams) | ||||
| 	if assert.Len(t, teams, 2) { | ||||
| 		assert.EqualValues(t, "Owners", teams[0].Name) | ||||
| 		assert.EqualValues(t, false, teams[0].CanCreateOrgRepo) | ||||
| 		assert.EqualValues(t, []string{"repo.code", "repo.issues", "repo.pulls", "repo.releases", "repo.wiki", "repo.ext_wiki", "repo.ext_issues"}, teams[0].Units) | ||||
| 		assert.EqualValues(t, "owner", teams[0].Permission) | ||||
| 
 | ||||
| 		assert.EqualValues(t, "test_team", teams[1].Name) | ||||
| 		assert.EqualValues(t, false, teams[1].CanCreateOrgRepo) | ||||
| 		assert.EqualValues(t, []string{"repo.issues"}, teams[1].Units) | ||||
| 		assert.EqualValues(t, "write", teams[1].Permission) | ||||
| 	} | ||||
| 
 | ||||
| 	// IsTeam
 | ||||
| 	url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "Test_Team", token) | ||||
| 	req = NewRequest(t, "GET", url) | ||||
| 	res = session.MakeRequest(t, req, http.StatusOK) | ||||
| 	var team *api.Team | ||||
| 	DecodeJSON(t, res, &team) | ||||
| 	assert.EqualValues(t, teams[1], team) | ||||
| 
 | ||||
| 	url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "NonExistingTeam", token) | ||||
| 	req = NewRequest(t, "GET", url) | ||||
| 	res = session.MakeRequest(t, req, http.StatusNotFound) | ||||
| 
 | ||||
| 	// AddTeam with user4
 | ||||
| 	url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token) | ||||
| 	req = NewRequest(t, "PUT", url) | ||||
| 	res = session.MakeRequest(t, req, http.StatusForbidden) | ||||
| 
 | ||||
| 	// AddTeam with user2
 | ||||
| 	user = models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | ||||
| 	session = loginUser(t, user.Name) | ||||
| 	token = getTokenForLoggedInUser(t, session) | ||||
| 	url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token) | ||||
| 	req = NewRequest(t, "PUT", url) | ||||
| 	res = session.MakeRequest(t, req, http.StatusNoContent) | ||||
| 	res = session.MakeRequest(t, req, http.StatusUnprocessableEntity) // test duplicate request
 | ||||
| 
 | ||||
| 	// DeleteTeam
 | ||||
| 	url = fmt.Sprintf("/api/v1/repos/%s/teams/%s?token=%s", publicOrgRepo.FullName(), "team1", token) | ||||
| 	req = NewRequest(t, "DELETE", url) | ||||
| 	res = session.MakeRequest(t, req, http.StatusNoContent) | ||||
| 	res = session.MakeRequest(t, req, http.StatusUnprocessableEntity) // test duplicate request
 | ||||
| } | ||||
|  | @ -727,6 +727,12 @@ func Routes() *web.Route { | |||
| 						Put(reqAdmin(), bind(api.AddCollaboratorOption{}), repo.AddCollaborator). | ||||
| 						Delete(reqAdmin(), repo.DeleteCollaborator) | ||||
| 				}, reqToken()) | ||||
| 				m.Group("/teams", func() { | ||||
| 					m.Get("", reqAnyRepoReader(), repo.ListTeams) | ||||
| 					m.Combo("/{team}").Get(reqAnyRepoReader(), repo.IsTeam). | ||||
| 						Put(reqAdmin(), repo.AddTeam). | ||||
| 						Delete(reqAdmin(), repo.DeleteTeam) | ||||
| 				}, reqToken()) | ||||
| 				m.Get("/raw/*", context.RepoRefForAPI, reqRepoReader(models.UnitTypeCode), repo.GetRawFile) | ||||
| 				m.Get("/archive/*", reqRepoReader(models.UnitTypeCode), repo.GetArchive) | ||||
| 				m.Combo("/forks").Get(repo.ListForks). | ||||
|  |  | |||
							
								
								
									
										233
									
								
								routers/api/v1/repo/teams.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								routers/api/v1/repo/teams.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,233 @@ | |||
| // Copyright 2020 The Gitea Authors. All rights reserved.
 | ||||
| // Use of this source code is governed by a MIT-style
 | ||||
| // license that can be found in the LICENSE file.
 | ||||
| 
 | ||||
| package repo | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 
 | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/convert" | ||||
| 	api "code.gitea.io/gitea/modules/structs" | ||||
| ) | ||||
| 
 | ||||
| // ListTeams list a repository's teams
 | ||||
| func ListTeams(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/teams repository repoListTeams
 | ||||
| 	// ---
 | ||||
| 	// summary: List a repository's teams
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: owner
 | ||||
| 	//   in: path
 | ||||
| 	//   description: owner of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: repo
 | ||||
| 	//   in: path
 | ||||
| 	//   description: name of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/TeamList"
 | ||||
| 
 | ||||
| 	if !ctx.Repo.Owner.IsOrganization() { | ||||
| 		ctx.Error(http.StatusMethodNotAllowed, "noOrg", "repo is not owned by an organization") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	teams, err := ctx.Repo.Repository.GetRepoTeams() | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	apiTeams := make([]*api.Team, len(teams)) | ||||
| 	for i := range teams { | ||||
| 		if err := teams[i].GetUnits(); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetUnits", err) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		apiTeams[i] = convert.ToTeam(teams[i]) | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.JSON(http.StatusOK, apiTeams) | ||||
| } | ||||
| 
 | ||||
| // IsTeam check if a team is assigned to a repository
 | ||||
| func IsTeam(ctx *context.APIContext) { | ||||
| 	// swagger:operation GET /repos/{owner}/{repo}/teams/{team} repository repoCheckTeam
 | ||||
| 	// ---
 | ||||
| 	// summary: Check if a team is assigned to a repository
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: owner
 | ||||
| 	//   in: path
 | ||||
| 	//   description: owner of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: repo
 | ||||
| 	//   in: path
 | ||||
| 	//   description: name of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: team
 | ||||
| 	//   in: path
 | ||||
| 	//   description: team name
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "200":
 | ||||
| 	//     "$ref": "#/responses/Team"
 | ||||
| 	//   "404":
 | ||||
| 	//     "$ref": "#/responses/notFound"
 | ||||
| 	//   "405":
 | ||||
| 	//     "$ref": "#/responses/error"
 | ||||
| 
 | ||||
| 	if !ctx.Repo.Owner.IsOrganization() { | ||||
| 		ctx.Error(http.StatusMethodNotAllowed, "noOrg", "repo is not owned by an organization") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	team := getTeamByParam(ctx) | ||||
| 	if team == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if team.HasRepository(ctx.Repo.Repository.ID) { | ||||
| 		if err := team.GetUnits(); err != nil { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetUnits", err) | ||||
| 			return | ||||
| 		} | ||||
| 		apiTeam := convert.ToTeam(team) | ||||
| 		ctx.JSON(http.StatusOK, apiTeam) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.NotFound() | ||||
| } | ||||
| 
 | ||||
| // AddTeam add a team to a repository
 | ||||
| func AddTeam(ctx *context.APIContext) { | ||||
| 	// swagger:operation PUT /repos/{owner}/{repo}/teams/{team} repository repoAddTeam
 | ||||
| 	// ---
 | ||||
| 	// summary: Add a team to a repository
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: owner
 | ||||
| 	//   in: path
 | ||||
| 	//   description: owner of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: repo
 | ||||
| 	//   in: path
 | ||||
| 	//   description: name of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: team
 | ||||
| 	//   in: path
 | ||||
| 	//   description: team name
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "204":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 	//   "422":
 | ||||
| 	//     "$ref": "#/responses/validationError"
 | ||||
| 	//   "405":
 | ||||
| 	//     "$ref": "#/responses/error"
 | ||||
| 
 | ||||
| 	changeRepoTeam(ctx, true) | ||||
| } | ||||
| 
 | ||||
| // DeleteTeam delete a team from a repository
 | ||||
| func DeleteTeam(ctx *context.APIContext) { | ||||
| 	// swagger:operation DELETE /repos/{owner}/{repo}/teams/{team} repository repoDeleteTeam
 | ||||
| 	// ---
 | ||||
| 	// summary: Delete a team from a repository
 | ||||
| 	// produces:
 | ||||
| 	// - application/json
 | ||||
| 	// parameters:
 | ||||
| 	// - name: owner
 | ||||
| 	//   in: path
 | ||||
| 	//   description: owner of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: repo
 | ||||
| 	//   in: path
 | ||||
| 	//   description: name of the repo
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// - name: team
 | ||||
| 	//   in: path
 | ||||
| 	//   description: team name
 | ||||
| 	//   type: string
 | ||||
| 	//   required: true
 | ||||
| 	// responses:
 | ||||
| 	//   "204":
 | ||||
| 	//     "$ref": "#/responses/empty"
 | ||||
| 	//   "422":
 | ||||
| 	//     "$ref": "#/responses/validationError"
 | ||||
| 	//   "405":
 | ||||
| 	//     "$ref": "#/responses/error"
 | ||||
| 
 | ||||
| 	changeRepoTeam(ctx, false) | ||||
| } | ||||
| 
 | ||||
| func changeRepoTeam(ctx *context.APIContext, add bool) { | ||||
| 	if !ctx.Repo.Owner.IsOrganization() { | ||||
| 		ctx.Error(http.StatusMethodNotAllowed, "noOrg", "repo is not owned by an organization") | ||||
| 	} | ||||
| 	if !ctx.Repo.Owner.RepoAdminChangeTeamAccess && !ctx.Repo.IsOwner() { | ||||
| 		ctx.Error(http.StatusForbidden, "noAdmin", "user is nor repo admin nor owner") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	team := getTeamByParam(ctx) | ||||
| 	if team == nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	repoHasTeam := team.HasRepository(ctx.Repo.Repository.ID) | ||||
| 	var err error | ||||
| 	if add { | ||||
| 		if repoHasTeam { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "alreadyAdded", fmt.Errorf("team '%s' is already added to repo", team.Name)) | ||||
| 			return | ||||
| 		} | ||||
| 		err = team.AddRepository(ctx.Repo.Repository) | ||||
| 	} else { | ||||
| 		if !repoHasTeam { | ||||
| 			ctx.Error(http.StatusUnprocessableEntity, "notAdded", fmt.Errorf("team '%s' was not added to repo", team.Name)) | ||||
| 			return | ||||
| 		} | ||||
| 		err = team.RemoveRepository(ctx.Repo.Repository.ID) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	ctx.Status(http.StatusNoContent) | ||||
| } | ||||
| 
 | ||||
| func getTeamByParam(ctx *context.APIContext) *models.Team { | ||||
| 	team, err := models.GetTeam(ctx.Repo.Owner.ID, ctx.Params(":team")) | ||||
| 	if err != nil { | ||||
| 		if models.IsErrTeamNotExist(err) { | ||||
| 			ctx.Error(http.StatusNotFound, "TeamNotExit", err) | ||||
| 			return nil | ||||
| 		} | ||||
| 		ctx.InternalServerError(err) | ||||
| 		return nil | ||||
| 	} | ||||
| 	return team | ||||
| } | ||||
|  | @ -8803,6 +8803,173 @@ | |||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/teams": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "List a repository's teams", | ||||
|         "operationId": "repoListTeams", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/TeamList" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/teams/{team}": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Check if a team is assigned to a repository", | ||||
|         "operationId": "repoCheckTeam", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "team name", | ||||
|             "name": "team", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "$ref": "#/responses/Team" | ||||
|           }, | ||||
|           "404": { | ||||
|             "$ref": "#/responses/notFound" | ||||
|           }, | ||||
|           "405": { | ||||
|             "$ref": "#/responses/error" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "put": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Add a team to a repository", | ||||
|         "operationId": "repoAddTeam", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "team name", | ||||
|             "name": "team", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           }, | ||||
|           "405": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "422": { | ||||
|             "$ref": "#/responses/validationError" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|       "delete": { | ||||
|         "produces": [ | ||||
|           "application/json" | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "repository" | ||||
|         ], | ||||
|         "summary": "Delete a team from a repository", | ||||
|         "operationId": "repoDeleteTeam", | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "owner of the repo", | ||||
|             "name": "owner", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "name of the repo", | ||||
|             "name": "repo", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           }, | ||||
|           { | ||||
|             "type": "string", | ||||
|             "description": "team name", | ||||
|             "name": "team", | ||||
|             "in": "path", | ||||
|             "required": true | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "$ref": "#/responses/empty" | ||||
|           }, | ||||
|           "405": { | ||||
|             "$ref": "#/responses/error" | ||||
|           }, | ||||
|           "422": { | ||||
|             "$ref": "#/responses/validationError" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "/repos/{owner}/{repo}/times": { | ||||
|       "get": { | ||||
|         "produces": [ | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue