feat(api): Add assignees APIs (#37330)

Follow
https://docs.github.com/en/enterprise-server@3.20/rest/issues/assignees?apiVersion=2022-11-28

Fix #33576 

And it also fixed some possible dead-lock problem.

---------

Signed-off-by: Lunny Xiao <xiaolunwen@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Nicolas <bircni@icloud.com>
Co-authored-by: Zettat123 <zettat123@gmail.com>
This commit is contained in:
Lunny Xiao
2026-06-08 23:12:09 -07:00
committed by GitHub
parent 611dfc9496
commit 49a0d19fa3
21 changed files with 1155 additions and 68 deletions
+87
View File
@@ -13,6 +13,7 @@ import (
"time"
auth_model "gitea.dev/models/auth"
"gitea.dev/models/db"
issues_model "gitea.dev/models/issues"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
@@ -35,6 +36,7 @@ func TestAPIIssue(t *testing.T) {
t.Run("IssueContentVersion", testAPIIssueContentVersion)
t.Run("CreateIssue", testAPICreateIssue)
t.Run("CreateIssueParallel", testAPICreateIssueParallel)
t.Run("IssueAssignees", testAPIIssueAssignees)
t.Run("IssueProjects", testAPIIssueProjects)
}
@@ -495,6 +497,91 @@ func testAPIIssueContentVersion(t *testing.T) {
})
}
func testAPIIssueAssignees(t *testing.T) {
issue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: issue.RepoID})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
session := loginUser(t, owner.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteIssue)
urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assignees", owner.Name, repo.Name, issue.Index)
getAssigneeIDs := func(issueID int64) []int64 {
assigneeIDs, err := issues_model.GetAssigneeIDsByIssue(t.Context(), issueID)
assert.NoError(t, err)
return assigneeIDs
}
t.Run("NonWriter", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"user40"}}).
AddTokenAuth(getUserToken(t, "user5", auth_model.AccessTokenScopeWriteIssue))
MakeRequest(t, req, http.StatusForbidden)
})
t.Run("MissingIssue", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/issues/99999/assignees", owner.Name, repo.Name), &api.IssueAssigneesOption{Assignees: []string{"user40"}}).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
})
t.Run("UnknownAssignee", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"does-not-exist"}}).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusUnprocessableEntity)
apiErr := DecodeJSON(t, resp, &api.APIError{})
assert.Equal(t, "user does not exist [uid: 0, name: does-not-exist]", apiErr.Message)
})
t.Run("OrganizationAssignee", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"org3"}}).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
checkReq := NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "org3")).AddTokenAuth(token)
MakeRequest(t, checkReq, http.StatusBadRequest)
})
t.Run("BlockedAssigneeIsAtomic", func(t *testing.T) {
blockedIssue := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 5})
blockedURL := fmt.Sprintf("/api/v1/repos/%s/%s/issues/%d/assignees", owner.Name, repo.Name, blockedIssue.Index)
blockedToken := getUserToken(t, "user40", auth_model.AccessTokenScopeWriteIssue)
assert.Empty(t, getAssigneeIDs(blockedIssue.ID))
assert.NoError(t, db.Insert(t.Context(), &user_model.Blocking{
BlockerID: owner.ID,
BlockeeID: 40,
}))
req := NewRequestWithJSON(t, "POST", blockedURL, &api.IssueAssigneesOption{Assignees: []string{"user1", "user2"}}).
AddTokenAuth(blockedToken)
MakeRequest(t, req, http.StatusForbidden)
assert.Empty(t, getAssigneeIDs(blockedIssue.ID))
})
t.Run("HappyPath", func(t *testing.T) {
req := NewRequestWithJSON(t, "POST", urlStr, &api.IssueAssigneesOption{Assignees: []string{"user40"}}).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusCreated)
apiIssue := DecodeJSON(t, resp, &api.Issue{})
assert.Len(t, apiIssue.Assignees, 2)
assert.ElementsMatch(t, []int64{1, 40}, []int64{apiIssue.Assignees[0].ID, apiIssue.Assignees[1].ID})
checkReq := NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "user40")).AddTokenAuth(token)
MakeRequest(t, checkReq, http.StatusNoContent)
// This endpoint checks assignability, not current assignment membership.
checkReq = NewRequest(t, "GET", fmt.Sprintf("%s/%s", urlStr, "user5")).AddTokenAuth(token)
MakeRequest(t, checkReq, http.StatusNoContent)
req = NewRequestWithJSON(t, "DELETE", urlStr, &api.IssueAssigneesOption{Assignees: []string{"user1"}}).
AddTokenAuth(token)
resp = MakeRequest(t, req, http.StatusOK)
apiIssue = DecodeJSON(t, resp, &api.Issue{})
assert.Len(t, apiIssue.Assignees, 1)
assert.Equal(t, int64(40), apiIssue.Assignees[0].ID)
})
}
func testAPIIssueProjects(t *testing.T) {
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
+16 -2
View File
@@ -721,11 +721,25 @@ func TestAPIRepoGetAssignees(t *testing.T) {
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadRepository)
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees", user.Name, repo.Name).
AddTokenAuth(token)
resp := MakeRequest(t, req, http.StatusOK)
assignees := DecodeJSON(t, resp, []*api.User{})
assert.Len(t, assignees, 2)
assert.Len(t, assignees, 1)
assignee := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees/%s", user.Name, repo.Name, assignee.Name).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNoContent)
nonAssignee := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees/%s", user.Name, repo.Name, nonAssignee.Name).
AddTokenAuth(token)
MakeRequest(t, req, http.StatusNotFound)
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/assignees/%s", user.Name, repo.Name, "org3").
AddTokenAuth(token)
MakeRequest(t, req, http.StatusBadRequest)
}