mirror of
https://gitea.com/gitea/tea.git
synced 2026-10-26 19:11:25 +01:00
fix: send a team's permission before its units (Gitea 28), hidden private-org team reviews, long non-ASCII repo descriptions, -R NAME vs directory guard, flake fallback
Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
12 files changed
+150
-8
No files matched your search
@@ -6,6 +6,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -238,6 +239,13 @@ func (s Repositories) Create(ctx context.Context, org string, opts RepositoryOpt
|
||||
Readme: opts.Readme, DefaultBranch: opts.DefaultBranch, TrustModel: gitea.TrustModel(opts.TrustModel),
|
||||
ObjectFormatName: opts.ObjectFormat,
|
||||
}
|
||||
// TODO(sdk): CreateRepoOption.Validate counts the description in bytes, but Gitea allows 2048
|
||||
// characters; a longer one is set by an edit once the repository exists.
|
||||
description := opts.Description
|
||||
later := len(description) > sdkDescriptionLimit
|
||||
if later {
|
||||
options.Description = ""
|
||||
}
|
||||
item, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.Repository, *gitea.Response, error) {
|
||||
if org != "" {
|
||||
return c.Repositories.CreateOrgRepo(ctx, org, options)
|
||||
@@ -251,9 +259,20 @@ func (s Repositories) Create(ctx context.Context, org string, opts RepositoryOpt
|
||||
}
|
||||
return Repository{}, Classify(err, "repository", "", "")
|
||||
}
|
||||
return repositoryFrom(item), nil
|
||||
created := repositoryFrom(item)
|
||||
if later {
|
||||
edited, err := s.Edit(ctx, created.FullName, RepositoryEdit{Description: &description})
|
||||
if err != nil {
|
||||
return created, fmt.Errorf("repository %s was created, but setting its description failed: %w", created.FullName, err)
|
||||
}
|
||||
return edited, nil
|
||||
}
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// sdkDescriptionLimit is the length in bytes that the SDK accepts for a new repository's description.
|
||||
const sdkDescriptionLimit = 2048
|
||||
|
||||
// Edit updates only explicitly supplied resource fields.
|
||||
func (s Repositories) Edit(ctx context.Context, repo string, opts RepositoryEdit) (Repository, error) {
|
||||
var mergeStyle *gitea.MergeStyle
|
||||
|
||||
@@ -6,6 +6,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -118,3 +119,24 @@ func TestRepositoryReadme(t *testing.T) {
|
||||
_, _, err = (Repositories{Client: client}).Readme(context.Background(), "alice/project", "develp")
|
||||
require.EqualError(t, err, `branch "develp" not found in alice/project`)
|
||||
}
|
||||
|
||||
func TestRepositoryCreateSetsADescriptionBeyondTheSDKByteLimitByEdit(t *testing.T) {
|
||||
description := strings.Repeat("日", 800) // 2400 bytes, 800 characters: Gitea accepts it, the SDK would not
|
||||
var methods []string
|
||||
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
methods = append(methods, r.Method)
|
||||
body := requestJSON(t, r)
|
||||
switch r.Method {
|
||||
case http.MethodPost:
|
||||
require.Empty(t, body["description"])
|
||||
_, _ = w.Write([]byte(`{"id":1,"name":"project","full_name":"alice/project"}`))
|
||||
case http.MethodPatch:
|
||||
require.Equal(t, description, body["description"])
|
||||
_, _ = w.Write([]byte(`{"id":1,"name":"project","full_name":"alice/project","description":"` + description + `"}`))
|
||||
}
|
||||
}))
|
||||
created, err := (Repositories{Client: client}).Create(context.Background(), "", RepositoryOptions{Name: "project", Description: description})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{http.MethodPost, http.MethodPatch}, methods)
|
||||
require.Equal(t, description, created.Description)
|
||||
}
|
||||
@@ -99,7 +99,8 @@ func (s Reviews) changeRequests(ctx context.Context, n int64, changes ReviewRequ
|
||||
|
||||
// CheckRequestTeams verifies that every named team exists in the repository's organization. Gitea
|
||||
// lets only organization members list teams, though outside collaborators and the pull request
|
||||
// author may request reviews; when the lookup is forbidden the server checks the names, as for gh.
|
||||
// author may request reviews; when the lookup is forbidden, or the organization is hidden from the
|
||||
// caller (a private organization answers 404 to non-members), the server checks the names, as for gh.
|
||||
func (s Reviews) CheckRequestTeams(ctx context.Context, names []string) error {
|
||||
owner, _, _ := strings.Cut(s.Repository, "/")
|
||||
_, isOrg, err := organization(ctx, s.Client, owner)
|
||||
@@ -110,10 +111,21 @@ func (s Reviews) CheckRequestTeams(ctx context.Context, names []string) error {
|
||||
return err
|
||||
}
|
||||
if !isOrg {
|
||||
// A private organization answers 404 to non-members, like a name nobody owns; a user that
|
||||
// exists is certainly not an organization.
|
||||
_, _, err := call(ctx, s.Client, func(ctx context.Context, c *gitea.Client) (*gitea.User, *gitea.Response, error) {
|
||||
return c.Users.GetUserInfo(ctx, owner)
|
||||
})
|
||||
if IsNotFound(err) || IsForbidden(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return errors.New("team reviews require an organization repository")
|
||||
}
|
||||
teams, err := (Teams{Client: s.Client}).ByName(ctx, owner)
|
||||
if IsForbidden(err) {
|
||||
if IsForbidden(err) || IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -95,3 +95,25 @@ func TestUnknownReviewerTeamRejectsBeforeUserRemoval(t *testing.T) {
|
||||
_, err := (Reviews{Client: transport.New(server.URL, "", false), Repository: "owner/repo"}).ChangeRequests(context.Background(), 1, changes)
|
||||
require.ErrorContains(t, err, `team "missing" is unavailable`)
|
||||
}
|
||||
|
||||
func TestReviewerTeamCheckSkippedForAHiddenPrivateOrganization(t *testing.T) {
|
||||
var posted bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.URL.Path == "/api/v1/orgs/owner" || r.URL.Path == "/api/v1/users/owner":
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte(`{"message":"GetUserByName"}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/api/v1/repos/owner/repo/pulls/1/requested_reviewers":
|
||||
posted = true
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_, _ = w.Write([]byte(`[]`))
|
||||
default:
|
||||
_, _ = w.Write([]byte(`{"number":1}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
changes := ReviewRequestChanges{Add: ReviewRequestInput{Teams: []string{"devs"}}}
|
||||
_, err := (Reviews{Client: transport.New(server.URL, "", false), Repository: "owner/repo"}).ChangeRequests(context.Background(), 1, changes)
|
||||
require.NoError(t, err)
|
||||
require.True(t, posted, "a non-member cannot see the organization, so the server decides")
|
||||
}
|
||||
@@ -250,6 +250,7 @@ func (s Teams) Create(ctx context.Context, org string, opts TeamOptions) (Team,
|
||||
// permissions when changing access to all repositories. Gitea replaces the whole policy on edit.
|
||||
func (s Teams) Edit(ctx context.Context, id int64, opts TeamOptions) (Team, error) {
|
||||
repositoryPolicy := opts.AllRepositories != nil && opts.Permission == nil
|
||||
changesPermission := opts.Permission != nil
|
||||
if repositoryPolicy || (opts.Units != nil && !opts.ReplaceUnits) {
|
||||
current, err := s.View(ctx, id)
|
||||
if err != nil {
|
||||
@@ -274,6 +275,22 @@ func (s Teams) Edit(ctx context.Context, id int64, opts TeamOptions) (Team, erro
|
||||
opts.Units = units
|
||||
}
|
||||
}
|
||||
// Gitea 28 keeps an admin team admin when one request changes its permission and sets units_map,
|
||||
// so change the permission alone first and then apply the units.
|
||||
if changesPermission && opts.Units != nil {
|
||||
first := TeamOptions{Name: opts.Name, Permission: opts.Permission}
|
||||
if first.Name == nil {
|
||||
current, err := s.View(ctx, id)
|
||||
if err != nil {
|
||||
return Team{}, err
|
||||
}
|
||||
first.Name = ¤t.Name
|
||||
}
|
||||
// TODO(sdk): EditTeamOption requires a name of at most 30 characters and a permission, and EditTeam should return the team.
|
||||
if err := sendJSON(ctx, s.Client, http.MethodPatch, fmt.Sprintf("teams/%d", id), first, nil); err != nil {
|
||||
return Team{}, err
|
||||
}
|
||||
}
|
||||
var team gitea.Team
|
||||
// TODO(sdk): EditTeamOption requires a name of at most 30 characters and a permission, and EditTeam should return the team.
|
||||
if err := sendJSON(ctx, s.Client, http.MethodPatch, fmt.Sprintf("teams/%d", id), opts, &team); err != nil {
|
||||
|
||||
@@ -179,3 +179,22 @@ func TestTeamFindSearchesTheOrganization(t *testing.T) {
|
||||
require.EqualError(t, err, `organization "nope" not found`)
|
||||
require.Equal(t, []string{"devops", "devops", "", "", "DEV", "DEV"}, queries)
|
||||
}
|
||||
|
||||
func TestTeamEditSendsThePermissionBeforeTheUnits(t *testing.T) {
|
||||
var bodies []map[string]any
|
||||
client := newTestClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/api/v1/teams/5", r.URL.Path)
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte(`{"id":5,"name":"devs","permission":"admin"}`))
|
||||
return
|
||||
}
|
||||
bodies = append(bodies, requestJSON(t, r))
|
||||
_, _ = w.Write([]byte(`{"id":5,"name":"devs","permission":"write"}`))
|
||||
}))
|
||||
permission := "write"
|
||||
_, err := (Teams{Client: client}).Edit(context.Background(), 5, TeamOptions{Permission: &permission, Units: map[string]string{"repo.wiki": "read"}, ReplaceUnits: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, bodies, 2)
|
||||
require.Equal(t, map[string]any{"name": "devs", "permission": "write"}, bodies[0], "Gitea 28 keeps an admin team admin when one request sets both")
|
||||
require.Equal(t, map[string]any{"repo.wiki": "read"}, bodies[1]["units_map"])
|
||||
}
|
||||
Reference in new issue
Block a user