Merge pull request #5588 from heaths/issue5587

status: ignore FORBIDDEN errors due to SAML enforcement
This commit is contained in:
Mislav Marohnić 2022-05-10 16:17:39 +02:00 committed by GitHub
commit f3099828f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 535 additions and 262 deletions

View file

@ -1,185 +1,153 @@
{
"data": {
"assignments": {
"edges": [
"nodes": [
{
"node": {
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
},
{
"node": {
"updatedAt": "2022-03-01T17:10:25Z",
"__typename": "PullRequest",
"title": "Board up RPD windows",
"number": 73,
"repository": {
"nameWithOwner": "rpd/todo"
}
"updatedAt": "2022-03-01T17:10:25Z",
"__typename": "PullRequest",
"title": "Board up RPD windows",
"number": 73,
"repository": {
"nameWithOwner": "rpd/todo"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2022-03-10T21:33:57Z",
"title": "yolo",
"number": 157,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
"__typename": "Issue",
"updatedAt": "2022-03-10T21:33:57Z",
"title": "yolo",
"number": 157,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
},
{
"node": {
"updatedAt": "2022-01-06T05:05:12Z",
"__typename": "PullRequest",
"title": "Issue Frecency",
"number": 4768,
"repository": {
"nameWithOwner": "cli/cli"
}
"updatedAt": "2022-01-06T05:05:12Z",
"__typename": "PullRequest",
"title": "Issue Frecency",
"number": 4768,
"repository": {
"nameWithOwner": "cli/cli"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2021-08-05T20:03:07Z",
"title": "Reducing zombie threat in Raccoon City",
"number": 514,
"repository": {
"nameWithOwner": "rpd/todo"
}
"__typename": "Issue",
"updatedAt": "2021-08-05T20:03:07Z",
"title": "Reducing zombie threat in Raccoon City",
"number": 514,
"repository": {
"nameWithOwner": "rpd/todo"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2021-10-16T23:20:35Z",
"title": "Repo garden for Windows",
"number": 3223,
"repository": {
"nameWithOwner": "cli/cli"
}
"__typename": "Issue",
"updatedAt": "2021-10-16T23:20:35Z",
"title": "Repo garden for Windows",
"number": 3223,
"repository": {
"nameWithOwner": "cli/cli"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2020-11-10T23:07:37Z",
"title": "welp",
"number": 74,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
"__typename": "Issue",
"updatedAt": "2020-11-10T23:07:37Z",
"title": "welp",
"number": 74,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2019-07-20T01:30:01Z",
"title": "Ability to send emails from helpdesk UI",
"number": 42,
"repository": {
"nameWithOwner": "tildetown/tildetown-admin"
}
"__typename": "Issue",
"updatedAt": "2019-07-20T01:30:01Z",
"title": "Ability to send emails from helpdesk UI",
"number": 42,
"repository": {
"nameWithOwner": "tildetown/tildetown-admin"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2019-07-22T19:37:45Z",
"title": "complete move to class based views",
"number": 22,
"repository": {
"nameWithOwner": "adreyer/arkestrator"
}
"__typename": "Issue",
"updatedAt": "2019-07-22T19:37:45Z",
"title": "complete move to class based views",
"number": 22,
"repository": {
"nameWithOwner": "adreyer/arkestrator"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2019-02-20T17:54:24Z",
"title": "`(every)` macro",
"number": 145,
"repository": {
"nameWithOwner": "vilmibm/tildemush"
}
"__typename": "Issue",
"updatedAt": "2019-02-20T17:54:24Z",
"title": "`(every)` macro",
"number": 145,
"repository": {
"nameWithOwner": "vilmibm/tildemush"
}
},
{
"node": {
"__typename": "Issue",
"updatedAt": "2019-02-20T18:10:53Z",
"title": "audit move rules",
"number": 79,
"repository": {
"nameWithOwner": "vilmibm/tildemush"
}
"__typename": "Issue",
"updatedAt": "2019-02-20T18:10:53Z",
"title": "audit move rules",
"number": 79,
"repository": {
"nameWithOwner": "vilmibm/tildemush"
}
}
]
},
"reviewRequested": {
"edges": [
"nodes": [
{
"node": {
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
},
{
"node": {
"updatedAt": "2022-02-18T19:38:20Z",
"__typename": "PullRequest",
"title": "Foobar",
"number": 1234,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
"updatedAt": "2022-02-18T19:38:20Z",
"__typename": "PullRequest",
"title": "Foobar",
"number": 1234,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
},
{
"node": {
"updatedAt": "2022-02-04T06:56:48Z",
"__typename": "PullRequest",
"title": "Welcome party for Leon",
"number": 50,
"repository": {
"nameWithOwner": "rpd/todo"
}
"updatedAt": "2022-02-04T06:56:48Z",
"__typename": "PullRequest",
"title": "Welcome party for Leon",
"number": 50,
"repository": {
"nameWithOwner": "rpd/todo"
}
},
{
"node": {
"updatedAt": "2021-12-20T10:30:39Z",
"__typename": "PullRequest",
"title": "Haircut for Leon",
"number": 49,
"repository": {
"nameWithOwner": "rpd/todo"
}
"updatedAt": "2021-12-20T10:30:39Z",
"__typename": "PullRequest",
"title": "Haircut for Leon",
"number": 49,
"repository": {
"nameWithOwner": "rpd/todo"
}
},
{
"node": {
"updatedAt": "2021-12-20T16:43:15Z",
"__typename": "PullRequest",
"title": "This pull request adds support for json output the to `gh pr checks` …",
"number": 4671,
"repository": {
"nameWithOwner": "cli/cli"
}
"updatedAt": "2021-12-20T16:43:15Z",
"__typename": "PullRequest",
"title": "This pull request adds support for json output the to `gh pr checks` …",
"number": 4671,
"repository": {
"nameWithOwner": "cli/cli"
}
}
]

View file

@ -0,0 +1,98 @@
{
"data": {
"assignments": {
"nodes": [
{
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
},
null,
{
"__typename": "Issue",
"updatedAt": "2022-03-10T21:33:57Z",
"title": "yolo",
"number": 157,
"repository": {
"nameWithOwner": "vilmibm/testing"
}
},
null
]
},
"reviewRequested": {
"nodes": [
{
"updatedAt": "2022-03-15T17:10:25Z",
"__typename": "PullRequest",
"title": "Pin extensions",
"number": 5272,
"repository": {
"nameWithOwner": "cli/cli"
}
},
null
]
}
},
"errors": [
{
"type": "FORBIDDEN",
"path": [
"assignments",
"nodes",
1
],
"extensions": {
"saml_failure": true
},
"locations": [
{
"line": 4,
"column": 5
}
],
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
},
{
"type": "FORBIDDEN",
"path": [
"assignments",
"nodes",
3
],
"extensions": {
"saml_failure": true
},
"locations": [
{
"line": 4,
"column": 5
}
],
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
},
{
"type": "FORBIDDEN",
"path": [
"reviewRequested",
"nodes",
1
],
"extensions": {
"saml_failure": true
},
"locations": [
{
"line": 4,
"column": 5
}
],
"message": "Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization."
}
]
}

View file

@ -2,19 +2,23 @@ package status
import (
"bytes"
"context"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"sync"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/charmbracelet/lipgloss"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/pkg/cmd/factory"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/set"
"github.com/cli/cli/v2/utils"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
@ -93,6 +97,7 @@ type Notification struct {
}
FullName string `json:"full_name"`
}
index int
}
type StatusItem struct {
@ -100,6 +105,7 @@ type StatusItem struct {
Identifier string // eg cli/cli#1234 or just 1234
preview string // eg This is the truncated body of something...
Reason string // only used in repo activity
index int
}
func (s StatusItem) Preview() string {
@ -155,6 +161,12 @@ func (rs Results) Swap(i, j int) {
rs[i], rs[j] = rs[j], rs[i]
}
type stringSet interface {
Len() int
Add(string)
ToSlice() []string
}
type StatusGetter struct {
Client *http.Client
cachedClient func(*http.Client, time.Duration) *http.Client
@ -166,6 +178,12 @@ type StatusGetter struct {
Mentions []StatusItem
ReviewRequests []StatusItem
RepoActivity []StatusItem
authErrors stringSet
authErrorsMu sync.Mutex
currentUsername string
usernameMu sync.Mutex
}
func NewStatusGetter(client *http.Client, hostname string, opts *StatusOptions) *StatusGetter {
@ -196,6 +214,13 @@ func (s *StatusGetter) ShouldExclude(repo string) bool {
}
func (s *StatusGetter) CurrentUsername() (string, error) {
s.usernameMu.Lock()
defer s.usernameMu.Unlock()
if s.currentUsername != "" {
return s.currentUsername, nil
}
cachedClient := s.CachedClient(time.Hour * 48)
cachingAPIClient := api.NewClientFromHTTP(cachedClient)
currentUsername, err := api.CurrentLoginName(cachingAPIClient, s.hostname())
@ -203,10 +228,11 @@ func (s *StatusGetter) CurrentUsername() (string, error) {
return "", fmt.Errorf("failed to get current username: %w", err)
}
s.currentUsername = currentUsername
return currentUsername, nil
}
func (s *StatusGetter) ActualMention(n Notification) (string, error) {
func (s *StatusGetter) ActualMention(commentURL string) (string, error) {
currentUsername, err := s.CurrentUsername()
if err != nil {
return "", err
@ -219,17 +245,15 @@ func (s *StatusGetter) ActualMention(n Notification) (string, error) {
resp := struct {
Body string
}{}
if err := c.REST(s.hostname(), "GET", n.Subject.LatestCommentURL, nil, &resp); err != nil {
if err := c.REST(s.hostname(), "GET", commentURL, nil, &resp); err != nil {
return "", err
}
var ret string
if strings.Contains(resp.Body, "@"+currentUsername) {
ret = resp.Body
return resp.Body, nil
}
return ret, nil
return "", nil
}
// These are split up by endpoint since it is along that boundary we parallelize
@ -244,16 +268,63 @@ func (s *StatusGetter) LoadNotifications() error {
query.Add("participating", "true")
query.Add("all", "true")
fetchWorkers := 10
ctx, abortFetching := context.WithCancel(context.Background())
defer abortFetching()
toFetch := make(chan Notification)
fetched := make(chan StatusItem)
wg := new(errgroup.Group)
for i := 0; i < fetchWorkers; i++ {
wg.Go(func() error {
for {
select {
case <-ctx.Done():
return nil
case n, ok := <-toFetch:
if !ok {
return nil
}
if actual, err := s.ActualMention(n.Subject.LatestCommentURL); actual != "" && err == nil {
// I'm so sorry
split := strings.Split(n.Subject.URL, "/")
fetched <- StatusItem{
Repository: n.Repository.FullName,
Identifier: fmt.Sprintf("%s#%s", n.Repository.FullName, split[len(split)-1]),
preview: actual,
index: n.index,
}
} else if err != nil {
var httpErr api.HTTPError
if errors.As(err, &httpErr) && httpErr.StatusCode == 403 {
s.addAuthError(httpErr.Message, factory.SSOURL())
} else {
abortFetching()
return fmt.Errorf("could not fetch comment: %w", err)
}
}
}
}
})
}
doneCh := make(chan struct{})
go func() {
for item := range fetched {
s.Mentions = append(s.Mentions, item)
}
close(doneCh)
}()
// this sucks, having to fetch so much :/ but it was the only way in my
// testing to really get enough mentions. I would love to be able to just
// filter for mentions but it does not seem like the notifications API can
// do that. I'd switch to the GraphQL version, but to my knowledge that does
// not work with PATs right now.
var ns []Notification
var resp []Notification
pages := 0
nIndex := 0
p := fmt.Sprintf("notifications?%s", query.Encode())
for pages < 3 {
for pages := 0; pages < 3; pages++ {
var resp []Notification
next, err := c.RESTWithNext(s.hostname(), "GET", p, nil, &resp)
if err != nil {
var httpErr api.HTTPError
@ -261,45 +332,36 @@ func (s *StatusGetter) LoadNotifications() error {
return fmt.Errorf("could not get notifications: %w", err)
}
}
ns = append(ns, resp...)
for _, n := range resp {
if n.Reason != "mention" {
continue
}
if s.Org != "" && n.Repository.Owner.Login != s.Org {
continue
}
if s.ShouldExclude(n.Repository.FullName) {
continue
}
n.index = nIndex
nIndex++
toFetch <- n
}
if next == "" || len(resp) < perPage {
break
}
pages++
p = next
}
s.Mentions = []StatusItem{}
for _, n := range ns {
if n.Reason != "mention" {
continue
}
if s.Org != "" && n.Repository.Owner.Login != s.Org {
continue
}
if s.ShouldExclude(n.Repository.FullName) {
continue
}
if actual, err := s.ActualMention(n); actual != "" && err == nil {
// I'm so sorry
split := strings.Split(n.Subject.URL, "/")
s.Mentions = append(s.Mentions, StatusItem{
Repository: n.Repository.FullName,
Identifier: fmt.Sprintf("%s#%s", n.Repository.FullName, split[len(split)-1]),
preview: actual,
})
} else if err != nil {
return fmt.Errorf("could not fetch comment: %w", err)
}
}
return nil
close(toFetch)
err := wg.Wait()
close(fetched)
<-doneCh
sort.Slice(s.Mentions, func(i, j int) bool {
return s.Mentions[i].index < s.Mentions[j].index
})
return err
}
const searchQuery = `
@ -323,18 +385,14 @@ fragment pr on PullRequest {
}
query AssignedSearch($searchAssigns: String!, $searchReviews: String!, $limit: Int = 25) {
assignments: search(first: $limit, type: ISSUE, query: $searchAssigns) {
edges {
node {
...issue
...pr
}
nodes {
...issue
...pr
}
}
reviewRequested: search(first: $limit, type: ISSUE, query: $searchReviews) {
edges {
node {
...pr
}
nodes {
...pr
}
}
}`
@ -360,37 +418,59 @@ func (s *StatusGetter) LoadSearchResults() error {
var resp struct {
Assignments struct {
Edges []struct {
Node SearchResult
}
Nodes []*SearchResult
}
ReviewRequested struct {
Edges []struct {
Node SearchResult
}
Nodes []*SearchResult
}
}
err := c.GraphQL(s.hostname(), searchQuery, variables, &resp)
if err != nil {
return fmt.Errorf("could not search for assignments: %w", err)
var gqlErrResponse *api.GraphQLErrorResponse
if errors.As(err, &gqlErrResponse) {
gqlErrors := make([]api.GraphQLError, 0, len(gqlErrResponse.Errors))
// Exclude any FORBIDDEN errors and show status for what we can.
for _, gqlErr := range gqlErrResponse.Errors {
if gqlErr.Type == "FORBIDDEN" {
s.addAuthError(gqlErr.Message, factory.SSOURL())
} else {
gqlErrors = append(gqlErrors, gqlErr)
}
}
if len(gqlErrors) == 0 {
err = nil
} else {
err = &api.GraphQLErrorResponse{Errors: gqlErrors}
}
}
if err != nil {
return fmt.Errorf("could not search for assignments: %w", err)
}
}
prs := []SearchResult{}
issues := []SearchResult{}
reviewRequested := []SearchResult{}
for _, e := range resp.Assignments.Edges {
if e.Node.Type == "Issue" {
issues = append(issues, e.Node)
} else if e.Node.Type == "PullRequest" {
prs = append(prs, e.Node)
} else {
panic("you shouldn't be here")
for _, node := range resp.Assignments.Nodes {
if node == nil {
continue // likely due to FORBIDDEN results
}
switch node.Type {
case "Issue":
issues = append(issues, *node)
case "PullRequest":
prs = append(prs, *node)
default:
return fmt.Errorf("unkown Assignments type: %q", node.Type)
}
}
for _, e := range resp.ReviewRequested.Edges {
reviewRequested = append(reviewRequested, e.Node)
for _, node := range resp.ReviewRequested.Nodes {
if node == nil {
continue // likely due to FORBIDDEN results
}
reviewRequested = append(reviewRequested, *node)
}
sort.Sort(Results(issues))
@ -506,6 +586,28 @@ func (s *StatusGetter) LoadEvents() error {
return nil
}
func (s *StatusGetter) addAuthError(msg, ssoURL string) {
s.authErrorsMu.Lock()
defer s.authErrorsMu.Unlock()
if s.authErrors == nil {
s.authErrors = set.NewStringSet()
}
if ssoURL != "" {
s.authErrors.Add(fmt.Sprintf("%s\nAuthorize in your web browser: %s", msg, ssoURL))
} else {
s.authErrors.Add(msg)
}
}
func (s *StatusGetter) HasAuthErrors() bool {
s.authErrorsMu.Lock()
defer s.authErrorsMu.Unlock()
return s.authErrors != nil && s.authErrors.Len() > 0
}
func statusRun(opts *StatusOptions) error {
client, err := opts.HttpClient()
if err != nil {
@ -522,36 +624,31 @@ func statusRun(opts *StatusOptions) error {
// TODO break out sections into individual subcommands
g := new(errgroup.Group)
g.Go(func() error {
if err := sg.LoadNotifications(); err != nil {
return fmt.Errorf("could not load notifications: %w", err)
}
return nil
})
g.Go(func() error {
if err := sg.LoadEvents(); err != nil {
return fmt.Errorf("could not load events: %w", err)
}
return nil
})
g.Go(func() error {
if err := sg.LoadSearchResults(); err != nil {
return fmt.Errorf("failed to search: %w", err)
}
return nil
})
opts.IO.StartProgressIndicator()
g.Go(func() error {
err := sg.LoadNotifications()
if err != nil {
err = fmt.Errorf("could not load notifications: %w", err)
}
return err
})
g.Go(func() error {
err := sg.LoadEvents()
if err != nil {
err = fmt.Errorf("could not load events: %w", err)
}
return err
})
g.Go(func() error {
err := sg.LoadSearchResults()
if err != nil {
err = fmt.Errorf("failed to search: %w", err)
}
return err
})
err = g.Wait()
opts.IO.StopProgressIndicator()
if err != nil {
return err
}
opts.IO.StopProgressIndicator()
cs := opts.IO.ColorScheme()
out := opts.IO.Out
@ -628,5 +725,13 @@ func statusRun(opts *StatusOptions) error {
fmt.Fprintln(out, lipgloss.JoinHorizontal(lipgloss.Top, rrSection, mSection))
fmt.Fprintln(out, raSection)
if sg.HasAuthErrors() {
errs := sg.authErrors.ToSlice()
sort.Strings(errs)
for _, msg := range errs {
fmt.Fprintln(out, cs.Gray(fmt.Sprintf("warning: %s", msg)))
}
}
return nil
}

View file

@ -2,10 +2,13 @@ package status
import (
"bytes"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/config"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/httpmock"
@ -96,7 +99,7 @@ func TestStatusRun(t *testing.T) {
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.StringResponse(`{"data": { "assignments": {"edges": [] }, "reviewRequested": {"edges": []}}}`))
httpmock.StringResponse(`{"data": { "assignments": {"nodes": [] }, "reviewRequested": {"nodes": []}}}`))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.StringResponse(`[]`))
@ -110,18 +113,6 @@ func TestStatusRun(t *testing.T) {
{
name: "something",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
@ -140,9 +131,6 @@ func TestStatusRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.FileResponse("./fixtures/search.json"))
@ -159,15 +147,6 @@ func TestStatusRun(t *testing.T) {
{
name: "exclude a repository",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
@ -183,9 +162,6 @@ func TestStatusRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.FileResponse("./fixtures/search.json"))
@ -212,9 +188,6 @@ func TestStatusRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.FileResponse("./fixtures/search.json"))
@ -235,12 +208,6 @@ func TestStatusRun(t *testing.T) {
{
name: "filter to an org",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
@ -253,9 +220,6 @@ func TestStatusRun(t *testing.T) {
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
httpmock.StringResponse(`{"body":"not a real mention"}`))
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.FileResponse("./fixtures/search.json"))
@ -271,6 +235,144 @@ func TestStatusRun(t *testing.T) {
},
wantOut: "Assigned Issues │ Assigned Pull Requests \nvilmibm/testing#157 yolo │ cli/cli#5272 Pin extensions \ncli/cli#3223 Repo garden...│ rpd/todo#73 Board up RPD windows \nrpd/todo#514 Reducing zo...│ cli/cli#4768 Issue Frecency \nvilmibm/testing#74 welp │ \nadreyer/arkestrator#22 complete mo...│ \n │ \nReview Requests │ Mentions \ncli/cli#5272 Pin extensions │ rpd/todo#110 hello @jillvalentine ...\nvilmibm/testing#1234 Foobar │ \nrpd/todo#50 Welcome party...│ \ncli/cli#4671 This pull req...│ \nrpd/todo#49 Haircut for Leon│ \n │ \nRepository Activity\nrpd/todo#5326 new PR Only write UTF-8 BOM on Windows where it is needed\n\n",
},
{
name: "forbidden errors",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
func(req *http.Request) (*http.Response, error) {
return &http.Response{
Request: req,
StatusCode: 403,
Body: ioutil.NopCloser(strings.NewReader(`{"message": "You don't have permission to access this resource."}`)),
Header: http.Header{
"Content-Type": []string{"application/json"},
"X-Github-Sso": []string{"required; url=http://click.me/auth"},
},
}, nil
})
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/4113"),
httpmock.StringResponse(`{"body":"this is a comment"}`))
reg.Register(
httpmock.REST("GET", "repos/cli/cli/issues/1096"),
httpmock.StringResponse(`{"body":"@jillvalentine hi"}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/comments/1065"),
httpmock.StringResponse(`{"body":"not a real mention"}`))
reg.Register(
httpmock.REST("GET", "repos/vilmibm/gh-screensaver/issues/comments/10"),
httpmock.StringResponse(`{"body":"a message for @jillvalentine"}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.FileResponse("./fixtures/search_forbidden.json"))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.FileResponse("./fixtures/notifications.json"))
reg.Register(
httpmock.REST("GET", "users/jillvalentine/received_events"),
httpmock.FileResponse("./fixtures/events.json"))
},
opts: &StatusOptions{},
wantOut: heredoc.Doc(`
Assigned Issues Assigned Pull Requests
vilmibm/testing#157 yolo cli/cli#5272 Pin extensions
Review Requests Mentions
cli/cli#5272 Pin extensions cli/cli#1096 @jillval...
vilmibm/gh-screensaver#15 a messag...
Repository Activity
rpd/todo#5326 new PR Only write UTF-8 BOM on W...
vilmibm/testing#5325 comment on Ability to sea... We are working on dedicat...
cli/cli#5319 comment on [Codespaces] D... Wondering if we shouldn't...
cli/cli#5300 new issue Terminal bell when a runn...
warning: Resource protected by organization SAML enforcement. You must grant your OAuth token access to this organization.
warning: You don't have permission to access this resource.
`),
},
{
name: "notification errors",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.REST("GET", "repos/rpd/todo/issues/110"),
httpmock.StatusStringResponse(429, `{
"message": "Too many requests."
}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.StringResponse(`{"data": { "assignments": {"nodes": [] }, "reviewRequested": {"nodes": []}}}`))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.StringResponse(`[
{
"reason": "mention",
"subject": {
"title": "Good",
"url": "https://api.github.com/repos/rpd/todo/issues/110",
"latest_comment_url": "https://api.github.com/repos/rpd/todo/issues/110",
"type": "Issue"
},
"repository": {
"full_name": "rpd/todo",
"owner": {
"login": "rpd"
}
}
}
]`))
reg.Register(
httpmock.REST("GET", "users/jillvalentine/received_events"),
httpmock.StringResponse(`[]`))
},
opts: &StatusOptions{},
wantErrMsg: "could not load notifications: could not fetch comment: HTTP 429 (https://api.github.com/repos/rpd/todo/issues/110)",
},
{
name: "search errors",
httpStubs: func(reg *httpmock.Registry) {
reg.Register(
httpmock.GraphQL("UserCurrent"),
httpmock.StringResponse(`{"data": {"viewer": {"login": "jillvalentine"}}}`))
reg.Register(
httpmock.GraphQL("AssignedSearch"),
httpmock.StringResponse(`{
"data": {
"assignments": {
"nodes": [
null
]
}
},
"errors": [
{
"type": "NOT FOUND",
"path": [
"assignments",
"nodes",
0
],
"message": "Not found."
}
]
}`))
reg.Register(
httpmock.REST("GET", "notifications"),
httpmock.StringResponse(`[]`))
reg.Register(
httpmock.REST("GET", "users/jillvalentine/received_events"),
httpmock.StringResponse(`[]`))
},
opts: &StatusOptions{},
wantErrMsg: "failed to search: could not search for assignments: GraphQL: Not found. (assignments.nodes.0)",
},
}
for _, tt := range tests {