558 lines
22 KiB
Go
558 lines
22 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestGraphQLRequestRecordsRateLimitHeaders(t *testing.T) {
|
|
reset := time.Now().Add(time.Hour).Unix()
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("X-RateLimit-Limit", "5000")
|
|
w.Header().Set("X-RateLimit-Remaining", "321")
|
|
w.Header().Set("X-RateLimit-Used", "4679")
|
|
w.Header().Set("X-RateLimit-Reset", fmtInt64(reset))
|
|
_, _ = w.Write([]byte(`{"data":{"viewer":{"login":"octocat"}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
var target struct {
|
|
Viewer struct{ Login string }
|
|
}
|
|
if err := client.query(context.Background(), "query { viewer { login } }", nil, &target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rate := client.RateLimit()
|
|
if rate.Limit != 5000 || rate.Remaining != 321 || rate.Used != 4679 ||
|
|
rate.ResetAt.Unix() != reset || rate.UpdatedAt.IsZero() {
|
|
t.Fatalf("rate limit = %#v", rate)
|
|
}
|
|
}
|
|
|
|
func fmtInt64(value int64) string {
|
|
return strconv.FormatInt(value, 10)
|
|
}
|
|
|
|
func TestListBranchesPaginatesAndMarksTheDefaultBranch(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests++
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(request.Query, "query RepositoryBranches") {
|
|
t.Fatalf("unexpected query: %s", request.Query)
|
|
}
|
|
if requests == 1 {
|
|
if request.Variables["after"] != nil {
|
|
t.Fatalf("first cursor = %#v", request.Variables["after"])
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{
|
|
"defaultBranchRef":{"name":"main"},
|
|
"refs":{"pageInfo":{"hasNextPage":true,"endCursor":"next"},"nodes":[
|
|
{"name":"feature/old","target":{"committedDate":"2024-01-01T00:00:00Z"}},
|
|
{"name":"main","target":{"committedDate":"2025-01-01T00:00:00Z"}}
|
|
]}
|
|
}}}`))
|
|
return
|
|
}
|
|
if request.Variables["after"] != "next" {
|
|
t.Fatalf("second cursor = %#v", request.Variables["after"])
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{
|
|
"defaultBranchRef":{"name":"main"},
|
|
"refs":{"pageInfo":{"hasNextPage":false},"nodes":[
|
|
{"name":"release/2.0","target":{"committedDate":"2026-07-28T00:00:00Z"}}
|
|
]}
|
|
}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
branches, err := client.ListBranches(context.Background(), "o", "r")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requests != 2 || len(branches) != 3 {
|
|
t.Fatalf("requests=%d branches=%#v", requests, branches)
|
|
}
|
|
if branches[0].Name != "main" || !branches[0].IsDefault {
|
|
t.Fatalf("default branch was not first and marked: %#v", branches)
|
|
}
|
|
wantUpdated := time.Date(2026, 7, 28, 0, 0, 0, 0, time.UTC)
|
|
if branches[1].Name != "release/2.0" || !branches[1].UpdatedAt.Equal(wantUpdated) {
|
|
t.Fatalf("fresh branch was not decoded and sorted: %#v", branches)
|
|
}
|
|
}
|
|
|
|
func TestListPullRequestsSearchesAssignedPRsInRepository(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if got := r.Header.Get("Authorization"); got != "Bearer secret" {
|
|
t.Fatalf("authorization = %q", got)
|
|
}
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := request.Variables["query"]; got != "is:pr is:open sort:updated-desc repo:o/r assignee:@me" {
|
|
t.Fatalf("search query = %q", got)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{
|
|
"viewer": map[string]any{"login": "zam"},
|
|
"search": map[string]any{"nodes": []any{
|
|
map[string]any{
|
|
"id": "1", "number": 1, "title": "assigned", "url": "u", "isDraft": false,
|
|
"updatedAt": "2026-01-01T00:00:00Z", "author": map[string]any{"login": "other"},
|
|
"repository": map[string]any{"name": "r", "nameWithOwner": "o/r", "owner": map[string]any{"login": "o"}},
|
|
"reviewThreads": map[string]any{"totalCount": 2},
|
|
},
|
|
}},
|
|
}})
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
prs, err := client.ListPullRequests(context.Background(), "o", "r", 50, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(prs) != 1 || prs[0].Number != 1 || prs[0].ReviewCount != 2 ||
|
|
prs[0].Owner != "o" || prs[0].Repository != "r" || prs[0].RepoWithOwner != "o/r" {
|
|
t.Fatalf("unexpected PRs: %#v", prs)
|
|
}
|
|
}
|
|
|
|
func TestListPullRequestsSearchesAllRepositoriesByDefault(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
query, _ := request.Variables["query"].(string)
|
|
if !strings.Contains(query, "assignee:@me") || strings.Contains(query, "repo:") {
|
|
t.Fatalf("global search query = %q", query)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"viewer":{"login":"zam"},"search":{"nodes":[]}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
if _, err := client.ListPullRequests(context.Background(), "", "", 50, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestListPullRequestsRejectsGlobalShowAll(t *testing.T) {
|
|
client := NewGitHubClient("unused", "secret")
|
|
if _, err := client.ListPullRequests(context.Background(), "", "", 50, true); err == nil {
|
|
t.Fatal("global --all search was accepted")
|
|
}
|
|
}
|
|
|
|
func TestListPullRequestsPaginatesToConfiguredLimit(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests++
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cursor := request.Variables["after"]
|
|
if requests == 1 {
|
|
if cursor != nil || request.Variables["first"] != float64(100) {
|
|
t.Fatalf("first page variables = %#v", request.Variables)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"viewer":{"login":"me"},"search":{
|
|
"pageInfo":{"hasNextPage":true,"endCursor":"next"},"nodes":[]}}}`))
|
|
return
|
|
}
|
|
if cursor != "next" || request.Variables["first"] != float64(50) {
|
|
t.Fatalf("second page variables = %#v", request.Variables)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"viewer":{"login":"me"},"search":{
|
|
"pageInfo":{"hasNextPage":false},"nodes":[]}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
if _, err := client.ListPullRequests(context.Background(), "", "", 150, false); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requests != 2 {
|
|
t.Fatalf("requests = %d, want 2", requests)
|
|
}
|
|
}
|
|
|
|
func TestGraphQLErrorsAreReturned(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"errors":[{"message":"no access"}]}`))
|
|
}))
|
|
defer server.Close()
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
_, err := client.ListPullRequests(context.Background(), "o", "r", 50, false)
|
|
if err == nil || !strings.Contains(err.Error(), "no access") {
|
|
t.Fatalf("error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestThreadWriteMutationsUseThreadIDs(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests++
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
input, _ := request.Variables["input"].(map[string]any)
|
|
switch {
|
|
case strings.Contains(request.Query, "unresolveReviewThread"):
|
|
if input["threadId"] != "thread" {
|
|
t.Fatalf("unresolve input = %#v", input)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"unresolveReviewThread":{"thread":{
|
|
"id":"thread","path":"a.go","isResolved":false,"viewerCanResolve":true
|
|
}}}}`))
|
|
case strings.Contains(request.Query, "resolveReviewThread"):
|
|
if input["threadId"] != "thread" {
|
|
t.Fatalf("resolve input = %#v", input)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"resolveReviewThread":{"thread":{
|
|
"id":"thread","path":"a.go","isResolved":true,"viewerCanUnresolve":true
|
|
}}}}`))
|
|
case strings.Contains(request.Query, "addPullRequestReviewThreadReply"):
|
|
if input["pullRequestReviewThreadId"] != "thread" || input["body"] != "reply body" {
|
|
t.Fatalf("reply input = %#v", input)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"addPullRequestReviewThreadReply":{"comment":{
|
|
"id":"comment","body":"reply body","createdAt":"2026-01-01T00:00:00Z",
|
|
"url":"https://example/comment","author":{"login":"me"}
|
|
}}}}`))
|
|
default:
|
|
t.Fatalf("unexpected mutation: %s", request.Query)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
resolved, err := client.SetThreadResolved(context.Background(), "thread", true)
|
|
if err != nil || !resolved.IsResolved || !resolved.ViewerCanUnresolve {
|
|
t.Fatalf("resolve result = %#v, error = %v", resolved, err)
|
|
}
|
|
unresolved, err := client.SetThreadResolved(context.Background(), "thread", false)
|
|
if err != nil || unresolved.IsResolved || !unresolved.ViewerCanResolve {
|
|
t.Fatalf("unresolve result = %#v, error = %v", unresolved, err)
|
|
}
|
|
comment, err := client.ReplyToThread(context.Background(), "thread", "reply body")
|
|
if err != nil || comment.ID != "comment" || comment.Author != "me" || comment.Body != "reply body" {
|
|
t.Fatalf("reply result = %#v, error = %v", comment, err)
|
|
}
|
|
if requests != 3 {
|
|
t.Fatalf("mutation requests = %d, want 3", requests)
|
|
}
|
|
}
|
|
|
|
func TestUpdatePullRequestMutatesTitleBodyAndBaseBranch(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(request.Query, "mutation UpdatePullRequest") {
|
|
t.Fatalf("unexpected mutation:\n%s", request.Query)
|
|
}
|
|
input := request.Variables["input"].(map[string]any)
|
|
if input["pullRequestId"] != "pr-id" || input["title"] != "New title" ||
|
|
input["body"] != "- [x] done" || input["baseRefName"] != "release" {
|
|
t.Fatalf("update input = %#v", input)
|
|
}
|
|
_, _ = w.Write([]byte(`{"data":{"updatePullRequest":{"pullRequest":{
|
|
"id":"pr-id","title":"New title","body":"- [x] done","baseRefName":"release",
|
|
"updatedAt":"2026-07-28T12:00:00Z","mergeable":"UNKNOWN","mergeStateStatus":"UNKNOWN"
|
|
}}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
got, err := client.UpdatePullRequest(context.Background(), "pr-id", PullRequestMetadata{
|
|
Title: "New title", Body: "- [x] done", BaseRef: "release",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Title != "New title" || got.Body != "- [x] done" || got.BaseRef != "release" ||
|
|
got.Mergeable != "UNKNOWN" || got.UpdatedAt.IsZero() {
|
|
t.Fatalf("updated pull request = %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestCheckContextsAndAnnotationsArePaginated(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch {
|
|
case strings.Contains(request.Query, "query CheckContextsPage"):
|
|
_, _ = w.Write([]byte(`{"data":{"node":{"contexts":{
|
|
"pageInfo":{"hasNextPage":false},"nodes":[{
|
|
"id":"check-2","name":"lint","conclusion":"FAILURE"
|
|
}]}}}}`))
|
|
case strings.Contains(request.Query, "query CheckAnnotationsPage"):
|
|
if request.Variables["after"] == nil {
|
|
_, _ = w.Write([]byte(`{"data":{"node":{"annotations":{
|
|
"pageInfo":{"hasNextPage":true,"endCursor":"annotation-next"},
|
|
"nodes":[{"path":"a.go","location":{"start":{"line":4},"end":{"line":4}},
|
|
"annotationLevel":"FAILURE","message":"first"}]
|
|
}}}}`))
|
|
} else {
|
|
_, _ = w.Write([]byte(`{"data":{"node":{"annotations":{
|
|
"pageInfo":{"hasNextPage":false},
|
|
"nodes":[{"path":"b.go","location":{"start":{"line":8},"end":{"line":8}},
|
|
"annotationLevel":"WARNING","message":"second"}]
|
|
}}}}`))
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected query: %s", request.Query)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
nodes, err := client.allCheckContexts(context.Background(), githubCheckContextConnection{
|
|
PageInfo: githubPageInfo{HasNextPage: true, EndCursor: "context-next"},
|
|
Nodes: []githubCheckContext{{ID: "check-1", Name: "tests", Conclusion: "SUCCESS"}},
|
|
}, "rollup")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
annotations, err := client.checkAnnotations(context.Background(), nodes[1].ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(nodes) != 2 || len(annotations) != 2 ||
|
|
annotations[0].Location.Start.Line != 4 ||
|
|
annotations[1].Location.End.Line != 8 {
|
|
t.Fatalf("paginated checks = %#v", nodes)
|
|
}
|
|
}
|
|
|
|
func TestCheckQueriesUseCurrentGitHubSchemaShape(t *testing.T) {
|
|
for name, query := range map[string]string{"annotations": checkAnnotationsPageQuery} {
|
|
if strings.Contains(query, "output {") ||
|
|
strings.Contains(query, "nodes { path startLine endLine annotationLevel") ||
|
|
!strings.Contains(query, "location { start { line column } end { line column } }") {
|
|
t.Fatalf("%s query uses obsolete CheckRun/CheckAnnotation fields:\n%s", name, query)
|
|
}
|
|
}
|
|
for name, query := range map[string]string{
|
|
"details": detailsQuery, "context page": checkContextsPageQuery,
|
|
} {
|
|
if strings.Contains(query, "annotations(first:") {
|
|
t.Fatalf("%s query eagerly loads annotations:\n%s", name, query)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequestPaginatesThreadsCommentsConversationAndReviews(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var request graphQLRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch {
|
|
case strings.Contains(request.Query, "query ReviewThreadsPage"):
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{"pullRequest":{"reviewThreads":{
|
|
"pageInfo":{"hasNextPage":false,"endCursor":null},
|
|
"nodes":[{"id":"thread-2","path":"b.go","line":20,"diffSide":"RIGHT",
|
|
"isResolved":false,"isOutdated":false,"viewerCanResolve":true,
|
|
"comments":{"pageInfo":{"hasNextPage":true,"endCursor":"comment-cursor"},
|
|
"nodes":[{"id":"review-comment-2a","body":"first","createdAt":"2026-01-02T00:00:00Z"}]}}]
|
|
}}}}}`))
|
|
case strings.Contains(request.Query, "query ReviewCommentsPage"):
|
|
_, _ = w.Write([]byte(`{"data":{"node":{"comments":{
|
|
"pageInfo":{"hasNextPage":false,"endCursor":null},
|
|
"nodes":[{"id":"review-comment-2b","body":"second","createdAt":"2026-01-03T00:00:00Z"}]
|
|
}}}}`))
|
|
case strings.Contains(request.Query, "query ConversationPage"):
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{"pullRequest":{"comments":{
|
|
"totalCount":2,"pageInfo":{"hasNextPage":false,"endCursor":null},
|
|
"nodes":[{"id":"conversation-2","body":"reply","createdAt":"2026-01-03T00:00:00Z"}]
|
|
}}}}}`))
|
|
case strings.Contains(request.Query, "query ReviewsPage"):
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{"pullRequest":{"reviews":{
|
|
"pageInfo":{"hasNextPage":false,"endCursor":null},
|
|
"nodes":[{"id":"review-2","body":"approved","state":"APPROVED","submittedAt":"2026-01-03T00:00:00Z"}]
|
|
}}}}}`))
|
|
default:
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{"viewerPermission":"WRITE","pullRequest":{
|
|
"id":"pr","number":1,"title":"PR","url":"u","createdAt":"2026-01-01T00:00:00Z",
|
|
"updatedAt":"2026-01-01T00:00:00Z","author":{"login":"alice"},
|
|
"assignees":{"nodes":[]},"labels":{"nodes":[]},"reviewRequests":{"nodes":[]},
|
|
"latestReviews":{"nodes":[]},"commits":{"totalCount":1,"nodes":[]},
|
|
"comments":{"totalCount":2,"pageInfo":{"hasNextPage":true,"endCursor":"conversation-cursor"},
|
|
"nodes":[{"id":"conversation-1","body":"start","createdAt":"2026-01-01T00:00:00Z"}]},
|
|
"reviews":{"pageInfo":{"hasNextPage":true,"endCursor":"review-cursor"},
|
|
"nodes":[{"id":"review-1","body":"changes","state":"CHANGES_REQUESTED","submittedAt":"2026-01-02T00:00:00Z"}]},
|
|
"reviewThreads":{"pageInfo":{"hasNextPage":true,"endCursor":"thread-cursor"},
|
|
"nodes":[{"id":"thread-1","path":"a.go","line":10,"diffSide":"RIGHT",
|
|
"isResolved":false,"isOutdated":false,
|
|
"comments":{"pageInfo":{"hasNextPage":false},
|
|
"nodes":[{"id":"review-comment-1","body":"fix","createdAt":"2026-01-01T00:00:00Z"}]}}]}
|
|
}}}}`))
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
got, err := client.GetPullRequest(context.Background(), "o", "r", 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Threads) != 2 || len(got.Threads[1].Comments) != 2 ||
|
|
len(got.Conversation) != 2 || len(got.Reviews) != 2 {
|
|
t.Fatalf("paginated details were incomplete: %#v", got)
|
|
}
|
|
if !got.Permissions.CanResolveAny || got.Permissions.Repository != "WRITE" {
|
|
t.Fatalf("permissions = %#v", got.Permissions)
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequestUsesOriginalLineAndMetadata(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{"viewerPermission":"WRITE","pullRequest":{
|
|
"id":"pr","number":9,"title":"Fix","url":"u","body":"body","isDraft":false,
|
|
"createdAt":"2025-12-01T00:00:00Z","updatedAt":"2026-01-01T00:00:00Z",
|
|
"mergeable":"MERGEABLE","mergeStateStatus":"CLEAN","reviewDecision":"APPROVED",
|
|
"additions":12,"deletions":4,"changedFiles":3,
|
|
"baseRefName":"main","headRefName":"fix","headRefOid":"abcdef0123456789",
|
|
"viewerCanUpdate":true,"viewerCanReact":true,"viewerCanSubscribe":true,
|
|
"viewerCanEnableAutoMerge":true,
|
|
"baseRef":{"branchProtectionRule":{"requiresApprovingReviews":true,
|
|
"requiredApprovingReviewCount":2,"requiresStatusChecks":true,
|
|
"requiresConversationResolution":true,"requiresCodeOwnerReviews":true}},
|
|
"author":{"login":"zam"},
|
|
"assignees":{"nodes":[{"login":"sam"}]},
|
|
"labels":{"nodes":[{"name":"bug"},{"name":"backend"}]},
|
|
"milestone":{"title":"v2"},
|
|
"comments":{"totalCount":5},
|
|
"reviewRequests":{"nodes":[{"requestedReviewer":{"login":"lee"}}]},
|
|
"latestReviews":{"nodes":[{"state":"CHANGES_REQUESTED","author":{"login":"pat"}}]},
|
|
"commits":{"totalCount":7,"nodes":[{"commit":{"oid":"abcdef0123456789",
|
|
"statusCheckRollup":{"state":"FAILURE","contexts":{"nodes":[
|
|
{"name":"tests","status":"COMPLETED","conclusion":"FAILURE","detailsUrl":"https://checks/tests"},
|
|
{"context":"legacy","state":"SUCCESS","targetUrl":"https://checks/legacy"}
|
|
]}}}}]},
|
|
"reviewThreads":{"pageInfo":{"hasNextPage":false},"nodes":[{
|
|
"id":"t","isResolved":false,"isOutdated":true,"path":"main.go",
|
|
"viewerCanResolve":true,
|
|
"line":null,"originalLine":42,"diffSide":"RIGHT",
|
|
"startLine":null,"originalStartLine":40,"startDiffSide":"RIGHT",
|
|
"comments":{"pageInfo":{"hasNextPage":false},"nodes":[{
|
|
"id":"c","body":"change this","diffHunk":"@@ -1 +1 @@","createdAt":"2026-01-01T00:00:00Z",
|
|
"url":"cu","author":{"login":"reviewer"},"outdated":true,
|
|
"line":100,"startLine":99,"originalLine":42,"originalStartLine":40,
|
|
"originalCommit":{"oid":"0123456789abcdef"},
|
|
"reactionGroups":[
|
|
{"content":"THUMBS_UP","viewerHasReacted":true,"reactors":{"totalCount":3}},
|
|
{"content":"EYES","viewerHasReacted":false,"reactors":{"totalCount":1}},
|
|
{"content":"HEART","viewerHasReacted":false,"reactors":{"totalCount":0}}
|
|
]
|
|
}]}
|
|
}]}
|
|
}}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
got, err := client.GetPullRequest(context.Background(), "o", "r", 9)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.CheckState != "FAILURE" || got.BaseRef != "main" || got.HeadRef != "fix" || got.ReviewDecision != "APPROVED" {
|
|
t.Fatalf("unexpected metadata: %#v", got)
|
|
}
|
|
if got.MergeState != "CLEAN" || got.Additions != 12 || got.Deletions != 4 ||
|
|
got.ChangedFiles != 3 || got.CommitCount != 7 || got.CommentCount != 5 ||
|
|
got.Milestone != "v2" || strings.Join(got.Labels, ",") != "bug,backend" ||
|
|
got.CreatedAt.IsZero() {
|
|
t.Fatalf("unexpected dashboard metadata: %#v", got)
|
|
}
|
|
if got.HeadOID != "abcdef0123456789" || len(got.Checks) != 2 ||
|
|
got.Checks[0].Name != "tests" || got.Checks[1].Name != "legacy" ||
|
|
got.Permissions.Repository != "WRITE" || !got.Permissions.CanUpdatePR ||
|
|
!got.Permissions.CanResolveAny || got.Requirements.ApprovalsRequired != 2 ||
|
|
!got.Requirements.RequiresConversation || !got.Requirements.RequiresCodeOwnerReview {
|
|
t.Fatalf("unexpected read capabilities: %#v", got)
|
|
}
|
|
if len(got.Threads) != 1 || got.Threads[0].Line != 42 || got.Threads[0].StartLine != 40 ||
|
|
got.Threads[0].DiffSide != "RIGHT" || got.Threads[0].IsTruncated {
|
|
t.Fatalf("unexpected thread: %#v", got.Threads)
|
|
}
|
|
comment := got.Threads[0].Comments[0]
|
|
if comment.OriginalLine != 42 || comment.OriginalStartLine != 40 ||
|
|
comment.OriginalCommitOID != "0123456789abcdef" || !comment.Outdated {
|
|
t.Fatalf("unexpected comment snapshot: %#v", comment)
|
|
}
|
|
if len(comment.Reactions) != 2 ||
|
|
comment.Reactions[0] != (ReactionSummary{Content: "THUMBS_UP", Count: 3, ViewerHasReacted: true}) ||
|
|
comment.Reactions[1] != (ReactionSummary{Content: "EYES", Count: 1}) {
|
|
t.Fatalf("unexpected comment reactions: %#v", comment.Reactions)
|
|
}
|
|
}
|
|
|
|
func TestGetPullRequestLoadsConflictFilesForConflictingPR(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
_, _ = w.Write([]byte(`{"data":{"repository":{
|
|
"url":"https://github.com/o/r","pullRequest":{
|
|
"id":"pr","number":3,"title":"Conflict","url":"u",
|
|
"mergeable":"CONFLICTING","baseRefName":"main","headRefName":"feature",
|
|
"headRefOid":"head123","baseRef":{"target":{"oid":"base123"}},
|
|
"comments":{"pageInfo":{"hasNextPage":false}},
|
|
"reviews":{"pageInfo":{"hasNextPage":false}},
|
|
"timelineItems":{"pageInfo":{"hasNextPage":false}},
|
|
"reviewThreads":{"pageInfo":{"hasNextPage":false}}
|
|
}
|
|
}}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewGitHubClient(server.URL, "secret")
|
|
client.conflicts = func(
|
|
_ context.Context,
|
|
repositoryURL string,
|
|
number int,
|
|
baseRef, baseOID, headOID, token string,
|
|
) ([]string, error) {
|
|
if repositoryURL != "https://github.com/o/r" || number != 3 ||
|
|
baseRef != "main" || baseOID != "base123" || headOID != "head123" ||
|
|
token != "secret" {
|
|
t.Fatalf(
|
|
"conflict loader arguments = %q, %d, %q, %q, %q, %q",
|
|
repositoryURL, number, baseRef, baseOID, headOID, token,
|
|
)
|
|
}
|
|
return []string{"src/conflict.go", "README.md"}, nil
|
|
}
|
|
|
|
got, err := client.GetPullRequest(context.Background(), "o", "r", 3)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.ConflictFiles) != 0 {
|
|
t.Fatalf("core refresh loaded conflict files eagerly: %#v", got.ConflictFiles)
|
|
}
|
|
enrichment := client.EnrichPullRequest(context.Background(), got)
|
|
if !reflect.DeepEqual(enrichment.ConflictFiles, []string{"src/conflict.go", "README.md"}) {
|
|
t.Fatalf("conflict files = %#v", enrichment.ConflictFiles)
|
|
}
|
|
}
|