From a4962a944002c3d3949520949f964aae7ff478ed Mon Sep 17 00:00:00 2001
From: Jimmy Praet <jimmy.praet@telenet.be>
Date: Fri, 13 Aug 2021 22:47:25 +0200
Subject: [PATCH] Add filter by owner and team to issue/pulls search endpoint
 (#16662)

* Filter by owner and team in API issue/pulls search

* Add integration test
---
 integrations/api_issue_test.go | 25 +++++++++++++++++++++++--
 models/fixtures/issue.yml      | 12 ++++++++++++
 models/fixtures/repository.yml |  2 +-
 routers/api/v1/repo/issue.go   | 39 +++++++++++++++++++++++++++++++++++++++
 templates/swagger/v1_json.tmpl | 12 ++++++++++++
 5 files changed, 87 insertions(+), 3 deletions(-)

diff --git a/integrations/api_issue_test.go b/integrations/api_issue_test.go
index 74512cafa3..245ab76c7b 100644
--- a/integrations/api_issue_test.go
+++ b/integrations/api_issue_test.go
@@ -206,7 +206,7 @@ func TestAPISearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String())
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.EqualValues(t, "14", resp.Header().Get("X-Total-Count"))
+	assert.EqualValues(t, "15", resp.Header().Get("X-Total-Count"))
 	assert.Len(t, apiIssues, 10) //there are more but 10 is page item limit
 
 	query.Add("limit", "20")
@@ -214,7 +214,7 @@ func TestAPISearchIssues(t *testing.T) {
 	req = NewRequest(t, "GET", link.String())
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
-	assert.Len(t, apiIssues, 14)
+	assert.Len(t, apiIssues, 15)
 
 	query = url.Values{"assigned": {"true"}, "state": {"all"}}
 	link.RawQuery = query.Encode()
@@ -236,6 +236,27 @@ func TestAPISearchIssues(t *testing.T) {
 	resp = session.MakeRequest(t, req, http.StatusOK)
 	DecodeJSON(t, resp, &apiIssues)
 	assert.Len(t, apiIssues, 2)
+
+	query = url.Values{"owner": {"user2"}} // user
+	link.RawQuery = query.Encode()
+	req = NewRequest(t, "GET", link.String())
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &apiIssues)
+	assert.Len(t, apiIssues, 6)
+
+	query = url.Values{"owner": {"user3"}} // organization
+	link.RawQuery = query.Encode()
+	req = NewRequest(t, "GET", link.String())
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &apiIssues)
+	assert.Len(t, apiIssues, 3)
+
+	query = url.Values{"owner": {"user3"}, "team": {"team1"}} // organization + team
+	link.RawQuery = query.Encode()
+	req = NewRequest(t, "GET", link.String())
+	resp = session.MakeRequest(t, req, http.StatusOK)
+	DecodeJSON(t, resp, &apiIssues)
+	assert.Len(t, apiIssues, 2)
 }
 
 func TestAPISearchIssuesWithLabels(t *testing.T) {
diff --git a/models/fixtures/issue.yml b/models/fixtures/issue.yml
index 946899d6ff..d5738d5db4 100644
--- a/models/fixtures/issue.yml
+++ b/models/fixtures/issue.yml
@@ -172,3 +172,15 @@
   is_pull: false
   created_unix: 1602935696
   updated_unix: 1602935696
+
+-
+  id: 15
+  repo_id: 5
+  index: 1
+  poster_id: 2
+  name: issue in repo not linked to team1
+  content: content
+  is_closed: false
+  is_pull: false
+  created_unix: 1602935696
+  updated_unix: 1602935696
diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index 492040316c..6860d00b30 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -73,7 +73,7 @@
   lower_name: repo5
   name: repo5
   is_private: true
-  num_issues: 0
+  num_issues: 1
   num_closed_issues: 0
   num_pulls: 0
   num_closed_pulls: 0
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index ff003b840b..e8ef2f3d05 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -87,6 +87,14 @@ func SearchIssues(ctx *context.APIContext) {
 	//   in: query
 	//   description: filter pulls requesting your review, default is false
 	//   type: boolean
+	// - name: owner
+	//   in: query
+	//   description: filter by owner
+	//   type: string
+	// - name: team
+	//   in: query
+	//   description: filter by team (requires organization owner parameter to be provided)
+	//   type: string
 	// - name: page
 	//   in: query
 	//   description: page number of results to return (1-based)
@@ -130,6 +138,37 @@ func SearchIssues(ctx *context.APIContext) {
 		opts.Private = true
 		opts.AllLimited = true
 	}
+	if ctx.FormString("owner") != "" {
+		owner, err := models.GetUserByName(ctx.FormString("owner"))
+		if err != nil {
+			if models.IsErrUserNotExist(err) {
+				ctx.Error(http.StatusBadRequest, "Owner not found", err)
+			} else {
+				ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
+			}
+			return
+		}
+		opts.OwnerID = owner.ID
+		opts.AllLimited = false
+		opts.AllPublic = false
+		opts.Collaborate = util.OptionalBoolFalse
+	}
+	if ctx.FormString("team") != "" {
+		if ctx.FormString("owner") == "" {
+			ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
+			return
+		}
+		team, err := models.GetTeam(opts.OwnerID, ctx.FormString("team"))
+		if err != nil {
+			if models.IsErrTeamNotExist(err) {
+				ctx.Error(http.StatusBadRequest, "Team not found", err)
+			} else {
+				ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
+			}
+			return
+		}
+		opts.TeamID = team.ID
+	}
 
 	repoIDs, _, err := models.SearchRepositoryIDs(opts)
 	if err != nil {
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d23d09bcfd..ee759ba6b1 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -1940,6 +1940,18 @@
             "in": "query"
           },
           {
+            "type": "string",
+            "description": "filter by owner",
+            "name": "owner",
+            "in": "query"
+          },
+          {
+            "type": "string",
+            "description": "filter by team (requires organization owner parameter to be provided)",
+            "name": "team",
+            "in": "query"
+          },
+          {
             "type": "integer",
             "description": "page number of results to return (1-based)",
             "name": "page",