package github import ( "context" "encoding/json" "errors" "net/http" "strings" "testing" "time" ) func TestPATClient_SubmitReview_SendsEventAndOptionalBody(t *testing.T) { cases := []struct { name string event string body string wantJSON map[string]string }{ {"approve without a body", "APPROVE", "", map[string]string{"event": "APPROVE"}}, { "request changes with a body", "REQUEST_CHANGES", "please fix", map[string]string{"event": "REQUEST_CHANGES", "body": "please fix"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusOK, `{}` }) if err := c.SubmitReview(context.Background(), "acme", "widget", 42, tc.event, tc.body); err != nil { t.Fatalf("SubmitReview: %v", err) } got := (*requests)[0] if got.Method != http.MethodPost { t.Errorf("method = %q, want POST", got.Method) } if got.Path != "/repos/acme/widget/pulls/42/reviews" { t.Errorf("path = %q", got.Path) } if got.Header.Get("Content-Type") != "application/json" { t.Errorf("content-type = %q, want application/json", got.Header.Get("Content-Type")) } var payload map[string]string if err := json.Unmarshal([]byte(got.Body), &payload); err != nil { t.Fatalf("decode request body %q: %v", got.Body, err) } if len(payload) != len(tc.wantJSON) { t.Fatalf("payload = %#v, want %#v", payload, tc.wantJSON) } for k, v := range tc.wantJSON { if payload[k] != v { t.Errorf("payload[%q] = %q, want %q", k, payload[k], v) } } }) } } func TestPATClient_SubmitReview_ErrorIncludesStatusAndBody(t *testing.T) { c, _ := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusUnprocessableEntity, `{"message":"Can not approve your own pull request"}` }) err := c.SubmitReview(context.Background(), "acme", "widget", 42, "APPROVE", "") if err == nil { t.Fatal("expected an error from 422") } if !strings.Contains(err.Error(), "422") || !strings.Contains(err.Error(), "own pull request") { t.Errorf("err = %v, want the status and upstream body surfaced", err) } } func TestPATClient_MergePR(t *testing.T) { t.Run("sends the merge method", func(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusOK, `{"status":"merged"}` }) if _, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{MergeMethod: "squash"}); err != nil { t.Fatalf("MergePR: %v", err) } got := (*requests)[0] if got.Method != http.MethodPut { t.Errorf("method = %q, want PUT", got.Method) } if got.Path != "/repos/acme/widget/pulls/42/merge-async" { t.Errorf("path = %q", got.Path) } if got.Body != `{"merge_action":"default","merge_method":"squash"}` { t.Errorf("body = %q, want queue-aware merge payload", got.Body) } }) t.Run("omits the merge method when unset", func(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusOK, `{"status":"merged"}` }) if _, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{}); err != nil { t.Fatalf("MergePR: %v", err) } if (*requests)[0].Body != `{"merge_action":"default"}` { t.Errorf("body = %q, want GitHub to choose direct or queued merge", (*requests)[0].Body) } }) t.Run("sends the expected head SHA", func(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusOK, `{"status":"merged"}` }) request := MergePRRequest{MergeMethod: "squash", ExpectedHeadSHA: "abc123"} if _, err := c.MergePR(context.Background(), "acme", "widget", 42, request); err != nil { t.Fatalf("MergePR: %v", err) } if got := (*requests)[0].Body; got != `{"merge_action":"default","merge_method":"squash","sha":"abc123"}` { t.Errorf("body = %q, want expected head SHA", got) } }) } // A merge rejection must stay recoverable as *GitHubAPIError so the HTTP // layer can map 405 (not mergeable) and 409 (conflict) onto a 409 response. func TestPATClient_MergePR_StatusIsRecoverable(t *testing.T) { for _, status := range []int{http.StatusMethodNotAllowed, http.StatusConflict} { t.Run(http.StatusText(status), func(t *testing.T) { c, _ := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return status, `{"message":"Pull Request is not mergeable"}` }) _, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{MergeMethod: "merge"}) var apiErr *GitHubAPIError if !errors.As(err, &apiErr) { t.Fatalf("err = %v, want a *GitHubAPIError", err) } if apiErr.StatusCode != status { t.Errorf("status = %d, want %d", apiErr.StatusCode, status) } if apiErr.Endpoint != "/repos/acme/widget/pulls/42/merge-async" { t.Errorf("endpoint = %q", apiErr.Endpoint) } if !strings.Contains(apiErr.Body, "not mergeable") { t.Errorf("body = %q, want the upstream message", apiErr.Body) } }) } } func TestPATClient_MergePR_AlreadyQueuedIsIdempotent(t *testing.T) { c, _ := newRecordingPATServerFunc(t, func(r *http.Request) (int, string) { if r.Method == http.MethodGet { return http.StatusOK, `{"status":"enqueued","details":{"message":"Pull request was added to the merge queue."}}` } return http.StatusConflict, `{"status":"pending","details":{"message":"Merge request enqueued.","uuid":"request-1","merge_method":"squash","merge_action":"default","expected_head_sha":"abc123"}}` }) outcome, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{MergeMethod: "squash"}) if err != nil || outcome != MergeOutcomeQueued { t.Fatalf("outcome = %q, err = %v, want queued", outcome, err) } } // @covers AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.3 func TestPATClient_MergePR_PollsNestedPendingResponse(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(r *http.Request) (int, string) { if r.Method == http.MethodGet { return http.StatusOK, `{"status":"merged","details":{"sha":"abc123"}}` } return http.StatusAccepted, `{"status":"pending","details":{"message":"Merge request enqueued.","uuid":"request-1","merge_method":"squash","merge_action":"default","expected_head_sha":"abc123"}}` }) outcome, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{MergeMethod: "squash"}) if err != nil || outcome != MergeOutcomeMerged { t.Fatalf("outcome = %q, err = %v, want merged", outcome, err) } if len(*requests) != 2 || (*requests)[1].Path != "/repos/acme/widget/pulls/42/merge-async/request-1" { t.Fatalf("requests = %#v, want a poll for request-1", *requests) } } // @covers AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-002.7 func TestPATClient_MergePR_PendingPollHonorsContextBeforeNextRequest(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusAccepted, `{"status":"pending","details":{"uuid":"request-1"}}` }) var gotDelay time.Duration c.mergePollWait = func(_ context.Context, delay time.Duration) error { gotDelay = delay return context.DeadlineExceeded } _, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{MergeMethod: "squash"}) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("MergePR error = %v, want context deadline", err) } if got := len(*requests); got != 1 { t.Fatalf("request count = %d, want only the initial request before cancellation", got) } if gotDelay != time.Second { t.Fatalf("poll delay = %s, want one second", gotDelay) } } // @covers AC-INTEGRATIONS-GITHUB-PR-MERGE-QUEUE-001.13 func TestPATClient_MergePR_ReportsNestedFailureMessage(t *testing.T) { c, _ := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusOK, `{"status":"failed","details":{"message":"Branch protection blocked the merge.","expected_head_sha":"abc123"}}` }) _, err := c.MergePR(context.Background(), "acme", "widget", 42, MergePRRequest{MergeMethod: "squash"}) if err == nil || !strings.Contains(err.Error(), "Branch protection blocked the merge.") || !strings.Contains(err.Error(), "abc123") { t.Fatalf("err = %v, want the nested provider message and expected head", err) } } func TestPATClient_CreateGist(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusCreated, `{"id":"abc123","html_url":"https://gist.github.com/abc123", "created_at":"2026-07-01T12:00:00Z"}` }) resp, err := c.CreateGist(context.Background(), CreateGistInput{ Description: "kandev share", Public: false, Files: map[string]GistFile{"README.md": {Content: "# hello"}}, }) if err != nil { t.Fatalf("CreateGist: %v", err) } if resp.ID != "abc123" || resp.HTMLURL != "https://gist.github.com/abc123" { t.Errorf("response = %#v", resp) } if !resp.CreatedAt.Equal(time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)) { t.Errorf("created_at = %v", resp.CreatedAt) } got := (*requests)[0] if got.Method != http.MethodPost || got.Path != "/gists" { t.Errorf("request = %s %s, want POST /gists", got.Method, got.Path) } var payload struct { Description string `json:"description"` Public bool `json:"public"` Files map[string]map[string]string `json:"files"` } if err := json.Unmarshal([]byte(got.Body), &payload); err != nil { t.Fatalf("decode payload %q: %v", got.Body, err) } if payload.Description != "kandev share" || payload.Public { t.Errorf("payload meta = %#v", payload) } if payload.Files["README.md"]["content"] != "# hello" { t.Errorf("payload files = %#v", payload.Files) } } func TestPATClient_CreateGist_ErrorIsTyped(t *testing.T) { c, _ := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusUnauthorized, `{"message":"Bad credentials"}` }) resp, err := c.CreateGist(context.Background(), CreateGistInput{Files: map[string]GistFile{}}) if resp != nil { t.Errorf("resp = %#v, want nil", resp) } var apiErr *GitHubAPIError if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusUnauthorized { t.Fatalf("err = %v, want a *GitHubAPIError with 401", err) } } func TestPATClient_DeleteGist(t *testing.T) { t.Run("deletes by id", func(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusNoContent, "" }) if err := c.DeleteGist(context.Background(), "abc123"); err != nil { t.Fatalf("DeleteGist: %v", err) } got := (*requests)[0] if got.Method != http.MethodDelete || got.Path != "/gists/abc123" { t.Errorf("request = %s %s, want DELETE /gists/abc123", got.Method, got.Path) } if got.Header.Get("Authorization") != "token test-token" { t.Errorf("DELETE must still carry auth, got %q", got.Header.Get("Authorization")) } }) t.Run("empty id never reaches the network", func(t *testing.T) { c, requests := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusNoContent, "" }) if err := c.DeleteGist(context.Background(), ""); err == nil { t.Fatal("expected an error for an empty gist id") } if len(*requests) != 0 { t.Errorf("requests = %d, want 0", len(*requests)) } }) // share.IsAlreadyGone matches on the typed 404 to treat an already-revoked // gist as a soft success rather than a 502. t.Run("404 stays recoverable as a typed error", func(t *testing.T) { c, _ := newRecordingPATServerFunc(t, func(*http.Request) (int, string) { return http.StatusNotFound, `{"message":"Not Found"}` }) err := c.DeleteGist(context.Background(), "gone") var apiErr *GitHubAPIError if !errors.As(err, &apiErr) || apiErr.StatusCode != http.StatusNotFound { t.Fatalf("err = %v, want a *GitHubAPIError with 404", err) } if apiErr.Endpoint != "/gists/gone" { t.Errorf("endpoint = %q, want /gists/gone", apiErr.Endpoint) } }) } func TestPATClient_ListPRReviews(t *testing.T) { c, requests := newRecordingPATServer(t, map[string]string{ "/repos/acme/widget/pulls/42/reviews": `[ {"id":1,"state":"APPROVED","body":"lgtm","submitted_at":"2026-01-05T10:00:00Z", "user":{"login":"alice","avatar_url":"https://avatars/alice"}}, {"id":2,"state":"CHANGES_REQUESTED","body":"nope","submitted_at":"2026-01-06T10:00:00Z", "user":{"login":"bob","avatar_url":""}} ]`, }) reviews, err := c.ListPRReviews(context.Background(), "acme", "widget", 42) if err != nil { t.Fatalf("ListPRReviews: %v", err) } if len(reviews) != 2 { t.Fatalf("reviews = %d, want 2", len(reviews)) } want := PRReview{ ID: 1, Author: "alice", AuthorAvatar: "https://avatars/alice", State: "APPROVED", Body: "lgtm", CreatedAt: time.Date(2026, 1, 5, 10, 0, 0, 0, time.UTC), } if reviews[0] != want { t.Errorf("reviews[0] = %#v, want %#v", reviews[0], want) } if reviews[1].ID != 2 || reviews[1].Author != "bob" || reviews[1].State != "CHANGES_REQUESTED" { t.Errorf("reviews[1] = %#v", reviews[1]) } if (*requests)[0].Path != "/repos/acme/widget/pulls/42/reviews" { t.Errorf("path = %q", (*requests)[0].Path) } } func TestPATClient_ListPRComments_MergesReviewAndIssueCommentsInTimeOrder(t *testing.T) { c, requests := newRecordingPATServer(t, map[string]string{ "/repos/acme/widget/pulls/42/comments": `[ {"id":20,"path":"main.go","line":7,"side":"RIGHT","body":"inline note", "created_at":"2026-01-05T12:00:00Z","updated_at":"2026-01-05T12:30:00Z", "in_reply_to_id":19,"html_url":"https://github.com/acme/widget/pull/42#discussion_r20", "user":{"login":"alice","avatar_url":"https://a","type":"User"}} ]`, "/repos/acme/widget/issues/42/comments": `[ {"id":10,"body":"conversation note","created_at":"2026-01-05T09:00:00Z", "updated_at":"2026-01-05T09:00:00Z","html_url":"https://github.com/acme/widget/pull/42#issuecomment-10", "user":{"login":"dependabot","avatar_url":"https://d","type":"Bot"}} ]`, }) comments, err := c.ListPRComments(context.Background(), "acme", "widget", 42, nil) if err != nil { t.Fatalf("ListPRComments: %v", err) } if len(comments) != 2 { t.Fatalf("comments = %d, want 2", len(comments)) } // The issue comment is older, so it must sort first. first, second := comments[0], comments[1] if first.ID != 10 || first.CommentType != commentTypeIssue { t.Errorf("comments[0] = %#v, want the older issue comment", first) } assertPRCommentHTMLURL(t, first, "https://github.com/acme/widget/pull/42#issuecomment-10") if !first.AuthorIsBot { t.Error("a Bot-typed user must set AuthorIsBot") } if second.ID != 20 || second.CommentType != commentTypeReview { t.Errorf("comments[1] = %#v, want the review comment", second) } if second.Path != "main.go" || second.Line != 7 || second.Side != "RIGHT" { t.Errorf("review comment location = %#v", second) } if second.InReplyTo == nil || *second.InReplyTo != 19 { t.Errorf("in_reply_to = %v, want 19", second.InReplyTo) } assertPRCommentHTMLURL(t, second, "https://github.com/acme/widget/pull/42#discussion_r20") if second.AuthorIsBot { t.Error("a User-typed author must not set AuthorIsBot") } for _, req := range *requests { if !strings.Contains(req.Query, "per_page=100") { t.Errorf("%s query = %q, want per_page=100", req.Path, req.Query) } if strings.Contains(req.Query, "since=") { t.Errorf("%s query = %q, want no since when the caller passed nil", req.Path, req.Query) } } } func TestPATClient_ListPRComments_AppendsSinceToBothEndpoints(t *testing.T) { c, requests := newRecordingPATServer(t, map[string]string{ "/repos/acme/widget/pulls/42/comments": `[]`, "/repos/acme/widget/issues/42/comments": `[]`, }) since := time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC) if _, err := c.ListPRComments(context.Background(), "acme", "widget", 42, &since); err != nil { t.Fatalf("ListPRComments: %v", err) } if len(*requests) != 2 { t.Fatalf("requests = %d, want 2", len(*requests)) } for _, req := range *requests { if got := parseQueryValues(t, req.Query).Get("since"); got != "2026-01-02T03:04:05Z" { t.Errorf("%s since = %q, want the RFC3339 timestamp", req.Path, got) } } } // The issue-comment leg runs after the review leg; its failure must abort the // whole call rather than silently returning a half-populated list. func TestPATClient_ListPRComments_IssueLegFailureAborts(t *testing.T) { c, _ := newRecordingPATServer(t, map[string]string{ "/repos/acme/widget/pulls/42/comments": `[]`, }) comments, err := c.ListPRComments(context.Background(), "acme", "widget", 42, nil) if err == nil { t.Fatal("expected an error when the issue-comments call fails") } if comments != nil { t.Errorf("comments = %#v, want nil", comments) } } func TestPATClient_ListPRFiles(t *testing.T) { c, requests := newRecordingPATServer(t, map[string]string{ "/repos/acme/widget/pulls/42/files": `[ {"filename":"main.go","status":"modified","additions":4,"deletions":1,"patch":"@@ -1 +1 @@\n-old\n+new"}, {"filename":"new.go","previous_filename":"old.go","status":"renamed","additions":0,"deletions":0} ]`, }) files, err := c.ListPRFiles(context.Background(), "acme", "widget", 42) if err != nil { t.Fatalf("ListPRFiles: %v", err) } if len(files) != 2 { t.Fatalf("files = %d, want 2", len(files)) } if files[0].Filename != "main.go" || files[0].Status != "modified" { t.Errorf("files[0] identity = %#v", files[0]) } if files[0].Additions != 4 || files[0].Deletions != 1 { t.Errorf("files[0] stats = (%d, %d), want (4, 1)", files[0].Additions, files[0].Deletions) } if !strings.Contains(files[0].Patch, "+new") { t.Errorf("files[0] patch = %q, want the hunk body preserved", files[0].Patch) } if files[1].OldPath != "old.go" || files[1].Status != "renamed" { t.Errorf("files[1] = %#v, want the rename source preserved", files[1]) } if !strings.Contains((*requests)[0].Query, "per_page=100") { t.Errorf("query = %q, want per_page=100", (*requests)[0].Query) } } func TestPATClient_ListPRFiles_Error(t *testing.T) { c, _ := newRecordingPATServer(t, nil) files, err := c.ListPRFiles(context.Background(), "acme", "widget", 42) if files != nil { t.Errorf("files = %#v, want nil", files) } if err == nil || !strings.Contains(err.Error(), "list PR files") { t.Fatalf("err = %v, want a wrapped list-PR-files error", err) } } func TestPATClient_ListPRCommits(t *testing.T) { c, requests := newRecordingPATServer(t, map[string]string{ "/repos/acme/widget/pulls/42/commits": `[ {"sha":"aaa","commit":{"message":"feat: add thing\n\nlong body","author":{"date":"2026-01-07T10:00:00Z"}}, "author":{"login":"alice"}}, {"sha":"bbb","commit":{"message":"fix: typo","author":{"date":"2026-01-08T10:00:00Z"}}} ]`, }) commits, err := c.ListPRCommits(context.Background(), "acme", "widget", 42) if err != nil { t.Fatalf("ListPRCommits: %v", err) } if len(commits) != 2 { t.Fatalf("commits = %d, want 2", len(commits)) } if commits[0].SHA != "aaa" || commits[0].AuthorLogin != "alice" { t.Errorf("commits[0] identity = %#v", commits[0]) } if commits[0].Message != "feat: add thing" { t.Errorf("commits[0] message = %q, want only the subject line", commits[0].Message) } if commits[0].AuthorDate != "2026-01-07T10:00:00Z" { t.Errorf("commits[0] author date = %q", commits[0].AuthorDate) } if commits[0].StatsAvailable { t.Error("the PR-commits list carries no per-commit stats; StatsAvailable must stay false") } // A commit with no matched GitHub account decodes with an empty login. if commits[1].AuthorLogin != "" || commits[1].Message != "fix: typo" { t.Errorf("commits[1] = %#v", commits[1]) } if !strings.Contains((*requests)[0].Query, "per_page=100") { t.Errorf("query = %q, want per_page=100", (*requests)[0].Query) } } func TestPATClient_ListPRCommits_FollowsLinkHeaderPages(t *testing.T) { c, requests := newLinkPaginatedPATServer(t, "/repos/acme/widget/pulls/42/commits", []string{ `[ {"sha":"aaa","commit":{"message":"first","author":{"date":"2026-01-07T10:00:00Z"}}}, {"sha":"bbb","commit":{"message":"second","author":{"date":"2026-01-08T10:00:00Z"}}} ]`, `[ {"sha":"ccc","commit":{"message":"provider head","author":{"date":"2026-01-09T10:00:00Z"}}} ]`, }) commits, err := c.ListPRCommits(context.Background(), "acme", "widget", 42) if err != nil { t.Fatalf("ListPRCommits: %v", err) } if len(commits) != 3 { t.Fatalf("commits = %d, want all pages (3)", len(commits)) } if commits[2].SHA != "ccc" { t.Errorf("last commit = %q, want the second-page provider head", commits[2].SHA) } if len(*requests) != 2 { t.Fatalf("requests = %d, want 2 pages", len(*requests)) } } func TestPATClient_ListPRCommits_Error(t *testing.T) { c, _ := newRecordingPATServer(t, nil) if _, err := c.ListPRCommits(context.Background(), "acme", "widget", 42); err == nil || !strings.Contains(err.Error(), "list PR commits") { t.Fatalf("err = %v, want a wrapped list-PR-commits error", err) } } func TestPATClient_ListRepoBranches_FollowsLinkHeaderPages(t *testing.T) { c, requests := newLinkPaginatedPATServer(t, "/repos/acme/widget/branches", []string{ `[{"name":"main"},{"name":"develop"}]`, `[{"name":"release/1.0"}]`, }) branches, err := c.ListRepoBranches(context.Background(), "acme", "widget") if err != nil { t.Fatalf("ListRepoBranches: %v", err) } want := []string{"main", "develop", "release/1.0"} if len(branches) != len(want) { t.Fatalf("branches = %#v, want %v", branches, want) } for i, name := range want { if branches[i].Name != name { t.Errorf("branches[%d] = %q, want %q", i, branches[i].Name, name) } } if len(*requests) != 2 { t.Fatalf("requests = %d, want 2 pages", len(*requests)) } if !strings.Contains((*requests)[0].Query, "per_page=100") { t.Errorf("first query = %q, want per_page=100", (*requests)[0].Query) } } func TestPATClient_ListRepoBranches_Error(t *testing.T) { c, _ := newRecordingPATServer(t, nil) branches, err := c.ListRepoBranches(context.Background(), "acme", "widget") if branches != nil { t.Errorf("branches = %#v, want nil", branches) } if err == nil || !strings.Contains(err.Error(), "list repo branches") { t.Fatalf("err = %v, want a wrapped list-repo-branches error", err) } } // Missing allow_* fields must read as false: a permission-gated repo response // that omits them would otherwise let the caller pick a disallowed method and // take a 405 from the merge endpoint. func TestPATClient_GetRepoMergeMethods(t *testing.T) { cases := []struct { name string body string want RepoMergeMethods }{ { "all allowed", `{"allow_merge_commit":true,"allow_squash_merge":true,"allow_rebase_merge":true}`, RepoMergeMethods{Merge: true, Squash: true, Rebase: true}, }, { "squash only", `{"allow_merge_commit":false,"allow_squash_merge":true,"allow_rebase_merge":false}`, RepoMergeMethods{Squash: true}, }, { "omitted fields read as disallowed", `{"full_name":"acme/widget"}`, RepoMergeMethods{}, }, { "explicit nulls read as disallowed", `{"allow_merge_commit":null,"allow_squash_merge":null,"allow_rebase_merge":null}`, RepoMergeMethods{}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { c, requests := newRecordingPATServer(t, map[string]string{"/repos/acme/widget": tc.body}) got, err := c.GetRepoMergeMethods(context.Background(), "acme", "widget") if err != nil { t.Fatalf("GetRepoMergeMethods: %v", err) } if got != tc.want { t.Errorf("methods = %#v, want %#v", got, tc.want) } if (*requests)[0].Path != "/repos/acme/widget" { t.Errorf("path = %q", (*requests)[0].Path) } }) } } func TestPATClient_GetRepoMergeMethods_Error(t *testing.T) { c, _ := newRecordingPATServer(t, nil) got, err := c.GetRepoMergeMethods(context.Background(), "acme", "widget") if got != (RepoMergeMethods{}) { t.Errorf("methods = %#v, want the zero value on error", got) } if err == nil || !strings.Contains(err.Error(), "get repo merge methods") { t.Fatalf("err = %v, want a wrapped merge-methods error", err) } }