* Correctly set the organization num repos Correctly set the organization num repos to the number of accessible repos for the user Fix #11194 Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @lunny Signed-off-by: Andrew Thornton <art27@cantab.net> * attempt to fix mssql Signed-off-by: Andrew Thornton <art27@cantab.net> * Update models/user.go * Explicit columns Signed-off-by: Andrew Thornton <art27@cantab.net> * Add test and fix 0 counted orgs Signed-off-by: Andrew Thornton <art27@cantab.net> * remove orgname from api Signed-off-by: Andrew Thornton <art27@cantab.net>tags/v1.21.12.1
| @@ -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) | |||||
| } | |||||
| } | |||||
| @@ -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) | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -712,18 +712,52 @@ func (u *User) GetOwnedOrganizations() (err error) { | |||||
| // GetOrganizations returns paginated organizations that user belongs to. | // GetOrganizations returns paginated organizations that user belongs to. | ||||
| func (u *User) GetOrganizations(opts *SearchOrganizationsOptions) error { | func (u *User) GetOrganizations(opts *SearchOrganizationsOptions) error { | ||||
| ous, err := GetOrgUsersByUserID(u.ID, opts) | |||||
| sess := x.NewSession() | |||||
| defer sess.Close() | |||||
| schema, err := x.TableInfo(new(User)) | |||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| groupByCols := &strings.Builder{} | |||||
| for _, col := range schema.Columns() { | |||||
| fmt.Fprintf(groupByCols, "`%s`.%s,", schema.Name, col.Name) | |||||
| } | |||||
| 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 | return nil | ||||
| } | } | ||||
| @@ -383,7 +383,7 @@ func orgAssignment(args ...bool) macaron.Handler { | |||||
| var err error | var err error | ||||
| if assignOrg { | if assignOrg { | ||||
| ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":orgname")) | |||||
| ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":org")) | |||||
| if err != nil { | if err != nil { | ||||
| if models.IsErrOrgNotExist(err) { | if models.IsErrOrgNotExist(err) { | ||||
| ctx.NotFound() | ctx.NotFound() | ||||
| @@ -857,7 +857,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Get("/users/:username/orgs", org.ListUserOrgs) | m.Get("/users/:username/orgs", org.ListUserOrgs) | ||||
| m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create) | m.Post("/orgs", reqToken(), bind(api.CreateOrgOption{}), org.Create) | ||||
| m.Get("/orgs", org.GetAll) | m.Get("/orgs", org.GetAll) | ||||
| m.Group("/orgs/:orgname", func() { | |||||
| m.Group("/orgs/:org", func() { | |||||
| m.Combo("").Get(org.Get). | m.Combo("").Get(org.Get). | ||||
| Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). | Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). | ||||
| Delete(reqToken(), reqOrgOwnership(), org.Delete) | Delete(reqToken(), reqOrgOwnership(), org.Delete) | ||||
| @@ -907,7 +907,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| }) | }) | ||||
| m.Group("/repos", func() { | m.Group("/repos", func() { | ||||
| m.Get("", org.GetTeamRepos) | m.Get("", org.GetTeamRepos) | ||||
| m.Combo("/:orgname/:reponame"). | |||||
| m.Combo("/:org/:reponame"). | |||||
| Put(org.AddTeamRepository). | Put(org.AddTeamRepository). | ||||
| Delete(org.RemoveTeamRepository) | Delete(org.RemoveTeamRepository) | ||||
| }) | }) | ||||