Merge main into watchpr auth failure branch
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

Keep both sides' additions to client_test.go and adopt main's
EditPROptions -> EditOptions rename.
This commit is contained in:
2026-09-19 16:40:44 +10:00
10 changed files with 760 additions and 66 deletions
+142 -6
View File
@@ -105,13 +105,13 @@ func TestEditPRSendsOnlySuppliedFields(t *testing.T) {
title, body, empty := "new title", "new body", ""
tests := []struct {
name string
opts EditPROptions
opts EditOptions
want map[string]any
}{
{"body only", EditPROptions{Body: &body}, map[string]any{"body": "new body"}},
{"title only", EditPROptions{Title: &title}, map[string]any{"title": "new title"}},
{"both", EditPROptions{Title: &title, Body: &body}, map[string]any{"title": "new title", "body": "new body"}},
{"explicit empty body is sent", EditPROptions{Body: &empty}, map[string]any{"body": ""}},
{"body only", EditOptions{Body: &body}, map[string]any{"body": "new body"}},
{"title only", EditOptions{Title: &title}, map[string]any{"title": "new title"}},
{"both", EditOptions{Title: &title, Body: &body}, map[string]any{"title": "new title", "body": "new body"}},
{"explicit empty body is sent", EditOptions{Body: &empty}, map[string]any{"body": ""}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -164,7 +164,7 @@ func TestEditPRAPIError(t *testing.T) {
title := "new title"
c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()}
_, err := c.EditPR("unkin/repo", 7, EditPROptions{Title: &title})
_, err := c.EditPR("unkin/repo", 7, EditOptions{Title: &title})
if err == nil {
t.Fatal("expected error on 404")
}
@@ -810,3 +810,139 @@ func TestWatchAbortsWhenTokenExpiresMidWatch(t *testing.T) {
t.Errorf("auth failure logged as a warning %d time(s); it must abort", warned)
}
}
func TestCreateIssueRequestBody(t *testing.T) {
var gotPath, gotMethod, gotAuth string
var gotBody CreateIssueOptions
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/repos/unkin/repo/issues", func(w http.ResponseWriter, r *http.Request) {
gotPath, gotMethod = r.URL.Path, r.Method
gotAuth = r.Header.Get("Authorization")
_ = json.NewDecoder(r.Body).Decode(&gotBody)
_, _ = io.WriteString(w, `{"number":12,"state":"open","title":"T","html_url":"https://git.unkin.net/unkin/repo/issues/12"}`)
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := &GiteaClient{BaseURL: srv.URL, Token: "gitea-abc", HTTP: srv.Client()}
issue, err := c.CreateIssue("unkin/repo", CreateIssueOptions{Title: "T", Body: "B"})
if err != nil {
t.Fatalf("CreateIssue: %v", err)
}
if gotMethod != http.MethodPost || gotPath != "/api/v1/repos/unkin/repo/issues" {
t.Errorf("request = %s %s, want POST /api/v1/repos/unkin/repo/issues", gotMethod, gotPath)
}
if gotAuth != "token gitea-abc" {
t.Errorf("auth header = %q, want 'token gitea-abc'", gotAuth)
}
if gotBody.Title != "T" || gotBody.Body != "B" {
t.Errorf("request body = %+v", gotBody)
}
if issue.Number != 12 || issue.HTMLURL != "https://git.unkin.net/unkin/repo/issues/12" {
t.Errorf("parsed issue = %+v", issue)
}
}
// Filing against a repo that does not exist (or that the token may not see)
// gets Gitea's 404, which must surface as a not-found error carrying the API's
// own message rather than a bare status.
func TestCreateIssueRepoNotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
_, _ = io.WriteString(w, `{"errors":null,"message":"user redirect does not exist [name: ghost]","url":"https://git.unkin.net/api/swagger"}`)
}))
defer srv.Close()
c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()}
_, err := c.CreateIssue("ghost/repo", CreateIssueOptions{Title: "T"})
if err == nil {
t.Fatal("expected error for a repo that does not exist")
}
if !IsNotFound(err) {
t.Errorf("IsNotFound(%v) = false, want true", err)
}
if !strings.Contains(err.Error(), "user redirect does not exist") {
t.Errorf("error %q should carry the API message", err)
}
}
// Any other non-2xx is a plain API failure: reported, not retried, and not
// mistaken for a missing repo.
func TestCreateIssueAPIError(t *testing.T) {
requests := 0
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/repos/unkin/repo/issues", func(w http.ResponseWriter, r *http.Request) {
requests++
w.WriteHeader(http.StatusUnprocessableEntity)
_, _ = io.WriteString(w, `{"errors":null,"message":"Validation Error: title is empty","url":"https://git.unkin.net/api/swagger"}`)
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()}
_, err := c.CreateIssue("unkin/repo", CreateIssueOptions{Title: "T"})
if err == nil {
t.Fatal("expected error on 422")
}
if IsNotFound(err) {
t.Errorf("a 422 must not read as not-found: %v", err)
}
if !strings.Contains(err.Error(), "Validation Error") {
t.Errorf("error %q should carry the API message", err)
}
if requests != 1 {
t.Errorf("requests = %d, want 1 (a 422 is not retried)", requests)
}
}
// An issue edit sends only the fields it was given, for the same reason a PR
// edit does: Gitea overwrites whatever key it receives.
func TestEditIssueSendsOnlySuppliedFields(t *testing.T) {
title, body := "new title", "new body"
tests := []struct {
name string
opts EditOptions
want map[string]any
}{
{"body only", EditOptions{Body: &body}, map[string]any{"body": "new body"}},
{"title only", EditOptions{Title: &title}, map[string]any{"title": "new title"}},
{"both", EditOptions{Title: &title, Body: &body}, map[string]any{"title": "new title", "body": "new body"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var gotBody map[string]any
var gotMethod, gotPath string
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/repos/unkin/repo/issues/12", func(w http.ResponseWriter, r *http.Request) {
gotMethod, gotPath = r.Method, r.URL.Path
_ = json.NewDecoder(r.Body).Decode(&gotBody)
_, _ = io.WriteString(w, `{"number":12,"title":"new title","html_url":"https://git.unkin.net/unkin/repo/issues/12"}`)
})
srv := httptest.NewServer(mux)
defer srv.Close()
c := &GiteaClient{BaseURL: srv.URL, Token: "t", HTTP: srv.Client()}
issue, err := c.EditIssue("unkin/repo", 12, tt.opts)
if err != nil {
t.Fatalf("EditIssue: %v", err)
}
if gotMethod != http.MethodPatch {
t.Errorf("method = %s, want PATCH", gotMethod)
}
if gotPath != "/api/v1/repos/unkin/repo/issues/12" {
t.Errorf("path = %q", gotPath)
}
if len(gotBody) != len(tt.want) {
t.Errorf("payload = %v, want exactly the supplied fields %v", gotBody, tt.want)
}
for k, v := range tt.want {
if gotBody[k] != v {
t.Errorf("payload[%q] = %v, want %v", k, gotBody[k], v)
}
}
if issue.Number != 12 || issue.HTMLURL == "" {
t.Errorf("parsed issue = %+v", issue)
}
})
}
}