diff --git a/gitea/issues.go b/gitea/issues.go index 51dbfdb..66cbaa2 100644 --- a/gitea/issues.go +++ b/gitea/issues.go @@ -133,6 +133,41 @@ func (c *Client) CreateIssue(owner, repo string, opts gitea.CreateIssueOption) ( return issue, nil } +// EditIssue edits an existing issue. +// Usage: EditIssue(...) +func (c *Client) EditIssue(owner, repo string, number int64, opts gitea.EditIssueOption) (*gitea.Issue, error) { + issue, _, err := c.api.EditIssue(owner, repo, number, opts) + if err != nil { + return nil, log.E("gitea.EditIssue", "failed to edit issue", err) + } + + return issue, nil +} + +// AssignIssue assigns an issue to the specified users. +// Usage: AssignIssue(...) +func (c *Client) AssignIssue(owner, repo string, number int64, assignees []string) error { + _, _, err := c.api.EditIssue(owner, repo, number, gitea.EditIssueOption{ + Assignees: assignees, + }) + if err != nil { + return log.E("gitea.AssignIssue", "failed to assign issue", err) + } + return nil +} + +// CreateIssueComment posts a comment on an issue or pull request. +// Usage: CreateIssueComment(...) +func (c *Client) CreateIssueComment(owner, repo string, issue int64, body string) error { + _, _, err := c.api.CreateIssueComment(owner, repo, issue, gitea.CreateIssueCommentOption{ + Body: body, + }) + if err != nil { + return log.E("gitea.CreateIssueComment", "failed to create comment", err) + } + return nil +} + // ListIssueComments returns all comments for an issue. // Usage: ListIssueComments(...) func (c *Client) ListIssueComments(owner, repo string, number int64) ([]*gitea.Comment, error) { @@ -253,6 +288,52 @@ func (c *Client) ListIssueCommentsIter(owner, repo string, number int64) iter.Se } } +// GetIssueLabels returns the labels currently attached to an issue. +// Usage: GetIssueLabels(...) +func (c *Client) GetIssueLabels(owner, repo string, number int64) ([]*gitea.Label, error) { + labels, _, err := c.api.GetIssueLabels(owner, repo, number, gitea.ListLabelsOptions{}) + if err != nil { + return nil, log.E("gitea.GetIssueLabels", "failed to get issue labels", err) + } + + return labels, nil +} + +// AddIssueLabels adds labels to an issue. +// Usage: AddIssueLabels(...) +func (c *Client) AddIssueLabels(owner, repo string, number int64, labelIDs []int64) error { + _, _, err := c.api.AddIssueLabels(owner, repo, number, gitea.IssueLabelsOption{ + Labels: labelIDs, + }) + if err != nil { + return log.E("gitea.AddIssueLabels", "failed to add labels to issue", err) + } + return nil +} + +// RemoveIssueLabel removes a label from an issue. +// Usage: RemoveIssueLabel(...) +func (c *Client) RemoveIssueLabel(owner, repo string, number, labelID int64) error { + _, err := c.api.DeleteIssueLabel(owner, repo, number, labelID) + if err != nil { + return log.E("gitea.RemoveIssueLabel", "failed to remove label from issue", err) + } + return nil +} + +// CloseIssue closes an issue by setting its state to closed. +// Usage: CloseIssue(...) +func (c *Client) CloseIssue(owner, repo string, number int64) error { + closed := gitea.StateClosed + _, _, err := c.api.EditIssue(owner, repo, number, gitea.EditIssueOption{ + State: &closed, + }) + if err != nil { + return log.E("gitea.CloseIssue", "failed to close issue", err) + } + return nil +} + // GetPullRequest returns a single pull request by number. // Usage: GetPullRequest(...) func (c *Client) GetPullRequest(owner, repo string, number int64) (*gitea.PullRequest, error) { diff --git a/gitea/issues_test.go b/gitea/issues_test.go index 23f17b1..c0adcc6 100644 --- a/gitea/issues_test.go +++ b/gitea/issues_test.go @@ -234,6 +234,132 @@ func TestClient_CreateIssue_Bad_ServerError_Good(t *testing.T) { assert.Contains(t, err.Error(), "failed to create issue") } +func TestClient_EditIssue_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + issue, err := client.EditIssue("test-org", "org-repo", 1, giteaSDK.EditIssueOption{ + Title: "Updated Title", + }) + require.NoError(t, err) + assert.NotNil(t, issue) +} + +func TestClient_EditIssue_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + _, err := client.EditIssue("test-org", "org-repo", 1, giteaSDK.EditIssueOption{ + Title: "Updated Title", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to edit issue") +} + +func TestClient_AssignIssue_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + err := client.AssignIssue("test-org", "org-repo", 1, []string{"dev1", "dev2"}) + require.NoError(t, err) +} + +func TestClient_AssignIssue_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + err := client.AssignIssue("test-org", "org-repo", 1, []string{"dev1"}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to assign issue") +} + +func TestClient_CreateIssueComment_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + err := client.CreateIssueComment("test-org", "org-repo", 1, "LGTM") + require.NoError(t, err) +} + +func TestClient_CreateIssueComment_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + err := client.CreateIssueComment("test-org", "org-repo", 1, "LGTM") + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to create comment") +} + +func TestClient_GetIssueLabels_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + labels, err := client.GetIssueLabels("test-org", "org-repo", 1) + require.NoError(t, err) + require.Len(t, labels, 1) + assert.Equal(t, "bug", labels[0].Name) +} + +func TestClient_GetIssueLabels_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + _, err := client.GetIssueLabels("test-org", "org-repo", 1) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to get issue labels") +} + +func TestClient_AddIssueLabels_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + err := client.AddIssueLabels("test-org", "org-repo", 1, []int64{1}) + require.NoError(t, err) +} + +func TestClient_AddIssueLabels_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + err := client.AddIssueLabels("test-org", "org-repo", 1, []int64{1}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to add labels to issue") +} + +func TestClient_RemoveIssueLabel_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + err := client.RemoveIssueLabel("test-org", "org-repo", 1, 1) + require.NoError(t, err) +} + +func TestClient_RemoveIssueLabel_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + err := client.RemoveIssueLabel("test-org", "org-repo", 1, 1) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to remove label from issue") +} + +func TestClient_CloseIssue_Good(t *testing.T) { + client, srv := newTestClient(t) + defer srv.Close() + + err := client.CloseIssue("test-org", "org-repo", 1) + require.NoError(t, err) +} + +func TestClient_CloseIssue_Bad_ServerError_Good(t *testing.T) { + client, srv := newErrorServer(t) + defer srv.Close() + + err := client.CloseIssue("test-org", "org-repo", 1) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to close issue") +} + func TestClient_ListPullRequests_Good(t *testing.T) { client, srv := newTestClient(t) defer srv.Close() diff --git a/gitea/testhelper_test.go b/gitea/testhelper_test.go index 9cedcc5..3e7d36c 100644 --- a/gitea/testhelper_test.go +++ b/gitea/testhelper_test.go @@ -85,18 +85,57 @@ func newGiteaMux() *http.ServeMux { // Single issue. mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues/1", func(w http.ResponseWriter, r *http.Request) { - jsonResponse(w, map[string]any{ - "id": 1, "number": 1, "title": "Issue 1", "state": "open", - "body": "First issue body", - }) + switch r.Method { + case http.MethodPatch: + jsonResponse(w, map[string]any{ + "id": 1, "number": 1, "title": "Issue 1", "state": "closed", + "body": "First issue body", + }) + default: + jsonResponse(w, map[string]any{ + "id": 1, "number": 1, "title": "Issue 1", "state": "open", + "body": "First issue body", + }) + } }) // Issue comments. mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues/1/comments", func(w http.ResponseWriter, r *http.Request) { - jsonResponse(w, []map[string]any{ - {"id": 100, "body": "comment 1", "user": map[string]any{"login": "user1"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z"}, - {"id": 101, "body": "comment 2", "user": map[string]any{"login": "user2"}, "created_at": "2026-01-02T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z"}, - }) + switch r.Method { + case http.MethodPost: + w.WriteHeader(http.StatusCreated) + jsonResponse(w, map[string]any{ + "id": 100, "body": "test comment", + "user": map[string]any{"login": "test-user"}, + }) + default: + jsonResponse(w, []map[string]any{ + {"id": 100, "body": "comment 1", "user": map[string]any{"login": "user1"}, "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:00:00Z"}, + {"id": 101, "body": "comment 2", "user": map[string]any{"login": "user2"}, "created_at": "2026-01-02T00:00:00Z", "updated_at": "2026-01-02T00:00:00Z"}, + }) + } + }) + + // Issue labels. + mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues/1/labels", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + jsonResponse(w, []map[string]any{ + {"id": 1, "name": "bug", "color": "#ff0000"}, + }) + default: + jsonResponse(w, []map[string]any{ + {"id": 1, "name": "bug", "color": "#ff0000"}, + }) + } + }) + + // Remove issue label. + mux.HandleFunc("/api/v1/repos/test-org/org-repo/issues/1/labels/1", func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodDelete { + w.WriteHeader(http.StatusNoContent) + return + } }) // Pull requests.