package main import ( "context" "encoding/json" "net/http" "net/http/httptest" "slices" "strings" "testing" ) func TestListRepositoryUsersCombinesReviewerAndAssigneeEligibility(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, "RepositoryReviewers"): if request.Variables["after"] == nil { _, _ = w.Write([]byte(`{"data":{"repository":{"collaborators":{ "pageInfo":{"hasNextPage":true,"endCursor":"next"}, "nodes":[ {"id":"u1","login":"alice","name":"Alice"}, {"id":"u2","login":"bob","name":"Bob"} ] }}}}`)) } else { _, _ = w.Write([]byte(`{"data":{"repository":{"collaborators":{ "pageInfo":{"hasNextPage":false}, "nodes":[{"id":"u4","login":"dave","name":"Dave"}] }}}}`)) } case strings.Contains(request.Query, "RepositoryAssignees"): _, _ = w.Write([]byte(`{"data":{"repository":{"assignableUsers":{ "pageInfo":{"hasNextPage":false}, "nodes":[ {"id":"u2","login":"bob","name":"Bob"}, {"id":"u3","login":"carol","name":"Carol"} ] }}}}`)) case strings.Contains(request.Query, "RepositoryContributions"): _, _ = w.Write([]byte(`{"data":{"repository":{"defaultBranchRef":{"target":{"history":{ "nodes":[ {"committedDate":"2026-07-28T12:00:00Z","additions":42, "author":{"user":{"login":"alice"}}}, {"committedDate":"2026-07-27T12:00:00Z","additions":8, "author":{"user":{"login":"alice"}}}, {"committedDate":"2026-06-01T12:00:00Z","additions":5, "author":{"user":{"login":"bob"}}} ] }}}}}}`)) default: t.Fatalf("unexpected query: %s", request.Query) } })) defer server.Close() client := NewGitHubClient(server.URL, "token") users, err := client.ListRepositoryUsers(context.Background(), "o", "r") if err != nil { t.Fatal(err) } if len(users) != 4 || users[0].Login != "alice" || !users[0].CanReview || users[0].CanAssign || users[1].Login != "bob" || !users[1].CanReview || !users[1].CanAssign || users[2].Login != "carol" || users[2].CanReview || !users[2].CanAssign || users[3].Login != "dave" || !users[3].CanReview { t.Fatalf("repository users = %#v", users) } if users[0].RecentCommits != 2 || users[0].RecentAdditions != 50 || users[0].LastContributionAt.IsZero() || users[1].RecentCommits != 1 { t.Fatalf("repository activity = %#v", users) } } func TestAllAssigneesPaginates(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, "AssigneesPage") { t.Fatalf("unexpected query: %s", request.Query) } _, _ = w.Write([]byte(`{"data":{"repository":{"pullRequest":{"assignees":{ "pageInfo":{"hasNextPage":false}, "nodes":[{"id":"u2","login":"bob","name":"Bob"}] }}}}}`)) })) defer server.Close() client := NewGitHubClient(server.URL, "token") users, err := client.allAssignees( context.Background(), "o", "r", 1, githubUserConnection{ PageInfo: githubPageInfo{HasNextPage: true, EndCursor: "next"}, Nodes: []githubActor{{Login: "alice"}}, }, ) if err != nil { t.Fatal(err) } if len(users) != 2 || users[0].Login != "alice" || users[1].Login != "bob" { t.Fatalf("assignees = %#v", users) } } func TestUpdatePullRequestPeoplePreservesUnchangedReviewers(t *testing.T) { var requests []string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { var input struct { Reviewers []string `json:"reviewers"` Assignees []string `json:"assignees"` } if err := json.NewDecoder(r.Body).Decode(&input); err != nil { t.Fatal(err) } switch { case strings.HasSuffix(r.URL.Path, "/requested_reviewers"): requests = append(requests, r.Method+":"+strings.Join(input.Reviewers, ",")) case strings.HasSuffix(r.URL.Path, "/issues/7"): requests = append(requests, r.Method+":"+strings.Join(input.Assignees, ",")) default: t.Fatalf("unexpected REST path: %s", r.URL.Path) } _, _ = w.Write([]byte(`{}`)) })) defer server.Close() client := NewGitHubClient(server.URL+"/api/graphql", "token") people, err := client.UpdatePullRequestPeople( context.Background(), "o", "r", 7, PullRequestPeopleUpdate{ CurrentReviewers: []string{"keep", "remove"}, CurrentAssignees: []string{"alice"}, Reviewers: []string{"keep", "add"}, Assignees: []string{"alice", "bob"}, }, ) if err != nil { t.Fatal(err) } want := []string{"POST:add", "DELETE:remove", "PATCH:alice,bob"} if !slices.Equal(requests, want) { t.Fatalf("REST requests = %v, want %v", requests, want) } if !slices.Equal(people.Reviewers, []string{"keep", "add"}) || !slices.Equal(people.Assignees, []string{"alice", "bob"}) { t.Fatalf("updated people = %#v", people) } } func TestUpdatePullRequestPeopleReportsSuccessfulPartialChanges(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodDelete { http.Error(w, "cannot remove", http.StatusUnprocessableEntity) return } _, _ = w.Write([]byte(`{}`)) })) defer server.Close() client := NewGitHubClient(server.URL+"/api/graphql", "token") people, err := client.UpdatePullRequestPeople( context.Background(), "o", "r", 7, PullRequestPeopleUpdate{ CurrentReviewers: []string{"keep", "remove"}, Reviewers: []string{"keep", "add"}, }, ) if err == nil || !strings.Contains(err.Error(), "remove reviewers") { t.Fatalf("partial update error = %v", err) } if !equalLoginSets(people.Reviewers, []string{"keep", "remove", "add"}) { t.Fatalf("partial reviewer state = %#v", people.Reviewers) } }