Compare commits

..

3 Commits

Author SHA1 Message Date
agent-company d0bcbd1ed0 feat: add POST /pulls/{owner}/{repo}/{index}/assignees route
Build and Push / test (pull_request) Successful in 1m46s
Build and Push / build (pull_request) Has been skipped
Register the PR assignees route pointing to the existing AssignIssue
handler, which works for both issues and PRs via the Gitea API.
Add integration tests covering valid, HTMX, and missing assignee cases.

Closes leeworks-agents/gitea-mobile#228

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 21:39:34 +00:00
AI-Manager d34916c276 Merge pull request 'feat: add MergePull() method to Gitea client (#187)' (#210) from feat/merge-pull-client-187 into master
Build and Push / test (push) Successful in 32s
Build and Push / build (push) Failing after 3s
2026-04-20 17:10:14 +00:00
agent-company d350500486 feat: add MergePull() method to Gitea client
Build and Push / test (pull_request) Successful in 39s
Build and Push / build (pull_request) Has been skipped
Add MergePull() that calls POST /repos/{owner}/{repo}/pulls/{index}/merge
with the specified merge style (merge, rebase, rebase-merge, squash).
Defaults to "merge" if no style specified. Includes unit tests for
success, default style, and error cases.

This is a prerequisite for #177 (merge PR button in UI) and #206
(POST /pulls merge handler).

Closes leeworks-agents/gitea-mobile#187

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 17:09:56 +00:00
4 changed files with 173 additions and 0 deletions
+34
View File
@@ -972,6 +972,40 @@ func (c *Client) SetIssueState(ctx context.Context, token, owner, repo string, i
return nil
}
// MergePull merges a pull request using the specified merge style.
// Valid styles: "merge", "rebase", "rebase-merge", "squash".
// If style is empty, defaults to "merge".
func (c *Client) MergePull(ctx context.Context, token, owner, repo string, index int64, style, title, message string) error {
if style == "" {
style = "merge"
}
payload := map[string]string{
"Do": style,
}
if title != "" {
payload["merge_message_field"] = title
}
if message != "" {
payload["merge_message_field"] = message
}
jsonData, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshaling merge request: %w", err)
}
path := fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", owner, repo, index)
resp, err := c.doRequest(ctx, token, http.MethodPost, path, strings.NewReader(string(jsonData)))
if err != nil {
return fmt.Errorf("merging pull request: %w", err)
}
resp.Body.Close()
c.InvalidateAll()
return nil
}
// AddComment creates a comment on an issue and returns the created Comment.
func (c *Client) AddComment(ctx context.Context, token, owner, repo string, index int64, body string) (*Comment, error) {
return c.PostComment(ctx, token, owner, repo, index, body)
+78
View File
@@ -1516,3 +1516,81 @@ func TestGetChangedFiles_Error(t *testing.T) {
t.Errorf("error should contain status code 404, got: %v", err)
}
}
func TestMergePull(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
t.Errorf("expected POST, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/owner1/repo1/pulls/5/merge" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
if r.Header.Get("Authorization") != "token test-token" {
t.Error("missing or wrong Authorization header")
}
var body map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode body: %v", err)
}
if body["Do"] != "squash" {
t.Errorf("expected Do=squash, got %q", body["Do"])
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
c := NewClient(server.URL)
c.setCache("pulls-org1", "should-be-invalidated")
err := c.MergePull(context.Background(), "test-token", "owner1", "repo1", 5, "squash", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify cache was invalidated.
_, ok := c.getFromCache("pulls-org1")
if ok {
t.Error("expected cache to be invalidated after MergePull")
}
}
func TestMergePull_DefaultStyle(t *testing.T) {
var receivedStyle string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]string
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode body: %v", err)
}
receivedStyle = body["Do"]
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
c := NewClient(server.URL)
err := c.MergePull(context.Background(), "test-token", "owner1", "repo1", 5, "", "", "")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if receivedStyle != "merge" {
t.Errorf("expected default style 'merge', got %q", receivedStyle)
}
}
func TestMergePull_Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
fmt.Fprintln(w, `{"message":"not mergeable"}`)
}))
defer server.Close()
c := NewClient(server.URL)
err := c.MergePull(context.Background(), "test-token", "owner1", "repo1", 5, "merge", "", "")
if err == nil {
t.Fatal("expected error for 405 response, got nil")
}
if !strings.Contains(err.Error(), "405") {
t.Errorf("error should contain status code 405, got: %v", err)
}
}
+1
View File
@@ -56,6 +56,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("GET /pulls/{owner}/{repo}/{index}", h.PullDetail)
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/review", h.SubmitReview)
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/state", h.SetPullState)
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/assignees", h.AssignIssue)
// Settings (handled separately for auth bypass).
settingsHandler := &SettingsHandler{
+60
View File
@@ -726,6 +726,66 @@ func TestIntegration_AssignIssue_MissingAssignee(t *testing.T) {
}
}
// --- Issue #228: Integration tests for POST /pulls/{owner}/{repo}/{index}/assignees ---
func TestIntegration_AssignPull_Valid(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/assignees", h.AssignIssue)
form := url.Values{"assignee": {"user1"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/assignees", form.Encode())
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusSeeOther {
t.Errorf("status = %d, want %d", w.Code, http.StatusSeeOther)
}
}
func TestIntegration_AssignPull_HTMX(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/assignees", h.AssignIssue)
form := url.Values{"assignee": {"user1"}}
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/assignees", form.Encode())
req.Header.Set("HX-Request", "true")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("status = %d, want %d", w.Code, http.StatusOK)
}
body := w.Body.String()
if !contains(body, "Assigned to user1") {
t.Errorf("expected 'Assigned to user1' in HTMX response, got: %s", body)
}
}
func TestIntegration_AssignPull_MissingAssignee(t *testing.T) {
h, srv := newTestHandlerWithMock(t)
defer srv.Close()
mux := http.NewServeMux()
mux.HandleFunc("POST /pulls/{owner}/{repo}/{index}/assignees", h.AssignIssue)
req := reqWithToken(http.MethodPost, "/pulls/test-org/repo1/1/assignees", "")
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
}
// --- Issue #118: Integration tests for CloseIssue and AddComment ---
func TestIntegration_CloseIssue(t *testing.T) {