Skip to content
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

Correctly set the organization num repos #11339

Merged
merged 9 commits into from
May 11, 2020
Merged
83 changes: 83 additions & 0 deletions integrations/api_helper_for_declarative_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,86 @@ func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFil
}
}
}

func doAPICreateOrganization(ctx APITestContext, options *api.CreateOrgOption, callback ...func(*testing.T, api.Organization)) func(t *testing.T) {
return func(t *testing.T) {
url := fmt.Sprintf("/api/v1/orgs?token=%s", ctx.Token)

req := NewRequestWithJSON(t, "POST", url, &options)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)

var contents api.Organization
DecodeJSON(t, resp, &contents)
if len(callback) > 0 {
callback[0](t, contents)
}
}
}

func doAPICreateOrganizationRepository(ctx APITestContext, orgName string, options *api.CreateRepoOption, callback ...func(*testing.T, api.Repository)) func(t *testing.T) {
return func(t *testing.T) {
url := fmt.Sprintf("/api/v1/orgs/%s/repos?token=%s", orgName, ctx.Token)

req := NewRequestWithJSON(t, "POST", url, &options)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)

var contents api.Repository
DecodeJSON(t, resp, &contents)
if len(callback) > 0 {
callback[0](t, contents)
}
}
}

func doAPICreateOrganizationTeam(ctx APITestContext, orgName string, options *api.CreateTeamOption, callback ...func(*testing.T, api.Team)) func(t *testing.T) {
return func(t *testing.T) {
url := fmt.Sprintf("/api/v1/orgs/%s/teams?token=%s", orgName, ctx.Token)

req := NewRequestWithJSON(t, "POST", url, &options)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)

var contents api.Team
DecodeJSON(t, resp, &contents)
if len(callback) > 0 {
callback[0](t, contents)
}
}
}

func doAPIAddUserToOrganizationTeam(ctx APITestContext, teamID int64, username string) func(t *testing.T) {
return func(t *testing.T) {
url := fmt.Sprintf("/api/v1/teams/%d/members/%s?token=%s", teamID, username, ctx.Token)

req := NewRequest(t, "PUT", url)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
}
}

func doAPIAddRepoToOrganizationTeam(ctx APITestContext, teamID int64, orgName, repoName string) func(t *testing.T) {
return func(t *testing.T) {
url := fmt.Sprintf("/api/v1/teams/%d/repos/%s/%s?token=%s", teamID, orgName, repoName, ctx.Token)

req := NewRequest(t, "PUT", url)
if ctx.ExpectedCode != 0 {
ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
return
}
ctx.Session.MakeRequest(t, req, http.StatusNoContent)
}
}
140 changes: 140 additions & 0 deletions integrations/org_count_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// 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 integrations

import (
"net/url"
"strings"
"testing"

"code.gitea.io/gitea/models"
api "code.gitea.io/gitea/modules/structs"
"github.com/stretchr/testify/assert"
)

func TestOrgCounts(t *testing.T) {
onGiteaRun(t, testOrgCounts)
}

func testOrgCounts(t *testing.T, u *url.URL) {
orgOwner := "user2"
orgName := "testOrg"
orgCollaborator := "user4"
ctx := NewAPITestContext(t, orgOwner, "repo1")

var ownerCountRepos map[string]int
var collabCountRepos map[string]int

t.Run("GetTheOwnersNumRepos", doCheckOrgCounts(orgOwner, map[string]int{},
false,
func(_ *testing.T, calcOrgCounts map[string]int) {
ownerCountRepos = calcOrgCounts
},
))
t.Run("GetTheCollaboratorsNumRepos", doCheckOrgCounts(orgCollaborator, map[string]int{},
false,
func(_ *testing.T, calcOrgCounts map[string]int) {
collabCountRepos = calcOrgCounts
},
))

t.Run("CreatePublicTestOrganization", doAPICreateOrganization(ctx, &api.CreateOrgOption{
UserName: orgName,
Visibility: "public",
}))

// Following the creation of the organization, the orgName must appear in the counts with 0 repos
ownerCountRepos[orgName] = 0

t.Run("AssertNumRepos0ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))

// the collaborator is not a collaborator yet
t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))

t.Run("CreateOrganizationPrivateRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
Name: "privateTestRepo",
AutoInit: true,
Private: true,
}))

ownerCountRepos[orgName] = 1
t.Run("AssertNumRepos1ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))

t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))

var testTeam api.Team

t.Run("CreateTeamForPublicTestOrganization", doAPICreateOrganizationTeam(ctx, orgName, &api.CreateTeamOption{
Name: "test",
Permission: "read",
Units: []string{"repo.code", "repo.issues", "repo.wiki", "repo.pulls", "repo.releases"},
CanCreateOrgRepo: true,
}, func(_ *testing.T, team api.Team) {
testTeam = team
}))

t.Run("AssertNoTestOrgReposForCollaborator", doCheckOrgCounts(orgCollaborator, collabCountRepos, true))

t.Run("AddCollboratorToTeam", doAPIAddUserToOrganizationTeam(ctx, testTeam.ID, orgCollaborator))

collabCountRepos[orgName] = 0
t.Run("AssertNumRepos0ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))

// Now create a Public Repo
t.Run("CreateOrganizationPublicRepo", doAPICreateOrganizationRepository(ctx, orgName, &api.CreateRepoOption{
Name: "publicTestRepo",
AutoInit: true,
}))

ownerCountRepos[orgName] = 2
t.Run("AssertNumRepos2ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
collabCountRepos[orgName] = 1
t.Run("AssertNumRepos1ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))

// Now add the testTeam to the privateRepo
t.Run("AddTestTeamToPrivateRepo", doAPIAddRepoToOrganizationTeam(ctx, testTeam.ID, orgName, "privateTestRepo"))

t.Run("AssertNumRepos2ForTestOrg", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
collabCountRepos[orgName] = 2
t.Run("AssertNumRepos2ForTestOrgForCollaborator", doCheckOrgCounts(orgOwner, ownerCountRepos, true))
}

func doCheckOrgCounts(username string, orgCounts map[string]int, strict bool, callback ...func(*testing.T, map[string]int)) func(t *testing.T) {
canonicalCounts := make(map[string]int, len(orgCounts))

for key, value := range orgCounts {
newKey := strings.TrimSpace(strings.ToLower(key))
canonicalCounts[newKey] = value
}

return func(t *testing.T) {
user := models.AssertExistsAndLoadBean(t, &models.User{
Name: username,
}).(*models.User)

user.GetOrganizations(&models.SearchOrganizationsOptions{All: true})

calcOrgCounts := map[string]int{}

for _, org := range user.Orgs {
calcOrgCounts[org.LowerName] = org.NumRepos
count, ok := canonicalCounts[org.LowerName]
if ok {
assert.True(t, count == org.NumRepos, "Number of Repos in %s is %d when we expected %d", org.Name, org.NumRepos, count)
} else {
assert.False(t, strict, "Did not expect to see %s with count %d", org.Name, org.NumRepos)
}
}

for key, value := range orgCounts {
_, seen := calcOrgCounts[strings.TrimSpace(strings.ToLower(key))]
assert.True(t, seen, "Expected to see %s with %d but did not", key, value)
}

if len(callback) > 0 {
callback[0](t, calcOrgCounts)
}
}
}
48 changes: 41 additions & 7 deletions models/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -712,18 +712,52 @@ func (u *User) GetOwnedOrganizations() (err error) {

// GetOrganizations returns paginated organizations that user belongs to.
func (u *User) GetOrganizations(opts *SearchOrganizationsOptions) error {
ous, err := GetOrgUsersByUserID(u.ID, opts)
sess := x.NewSession()
zeripath marked this conversation as resolved.
Show resolved Hide resolved
defer sess.Close()

schema, err := x.TableInfo(new(User))
if err != nil {
return err
}
groupByCols := &strings.Builder{}
for _, col := range schema.Columns() {
fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col.Name)
zeripath marked this conversation as resolved.
Show resolved Hide resolved
}
groupByStr := groupByCols.String()
groupByStr = groupByStr[0 : len(groupByStr)-1]

u.Orgs = make([]*User, len(ous))
for i, ou := range ous {
u.Orgs[i], err = GetUserByID(ou.OrgID)
if err != nil {
return err
}
sess.Select("`user`.*, count(repo_id) as org_count").
Table("user").
Join("INNER", "org_user", "`org_user`.org_id=`user`.id").
Join("LEFT", builder.
Select("id as repo_id, owner_id as repo_owner_id").
From("repository").
Where(accessibleRepositoryCondition(u)), "`repository`.repo_owner_id = `org_user`.org_id").
And("`org_user`.uid=?", u.ID).
GroupBy(groupByStr)
if opts.PageSize != 0 {
sess = opts.setSessionPagination(sess)
}
type OrgCount struct {
User `xorm:"extends"`
OrgCount int
}
orgCounts := make([]*OrgCount, 0, 10)

if err := sess.
Asc("`user`.name").
Find(&orgCounts); err != nil {
return err
}

orgs := make([]*User, len(orgCounts))
for i, orgCount := range orgCounts {
orgCount.User.NumRepos = orgCount.OrgCount
orgs[i] = &orgCount.User
}

u.Orgs = orgs

return nil
}

Expand Down
6 changes: 3 additions & 3 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ func orgAssignment(args ...bool) macaron.Handler {

var err error
if assignOrg {
ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":orgname"))
ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":org"))
if err != nil {
if models.IsErrOrgNotExist(err) {
ctx.NotFound()
Expand Down Expand Up @@ -857,7 +857,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/users/:username/orgs", org.ListUserOrgs)
m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create)
m.Get("/orgs", org.GetAll)
m.Group("/orgs/:orgname", func() {
m.Group("/orgs/:org", func() {
m.Combo("").Get(org.Get).
Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit).
Delete(reqToken(), reqOrgOwnership(), org.Delete)
Expand Down Expand Up @@ -907,7 +907,7 @@ func RegisterRoutes(m *macaron.Macaron) {
})
m.Group("/repos", func() {
m.Get("", org.GetTeamRepos)
m.Combo("/:orgname/:reponame").
m.Combo("/:org/:reponame").
Put(org.AddTeamRepository).
Delete(org.RemoveTeamRepository)
})
Expand Down