Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 0 additions & 38 deletions api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"regexp"
"strings"

"github.com/cli/cli/v2/pkg/set"
ghAPI "github.com/cli/go-gh/v2/pkg/api"
ghauth "github.com/cli/go-gh/v2/pkg/auth"
)
Expand Down Expand Up @@ -181,10 +180,6 @@ func handleResponse(err error) error {

var gqlErr *ghAPI.GraphQLError
if errors.As(err, &gqlErr) {
scopeErr := GenerateScopeErrorForGQL(gqlErr)
if scopeErr != nil {
return scopeErr
}
return GraphQLError{
GraphQLError: gqlErr,
}
Expand All @@ -193,39 +188,6 @@ func handleResponse(err error) error {
return err
}

func GenerateScopeErrorForGQL(gqlErr *ghAPI.GraphQLError) error {
missing := set.NewStringSet()
for _, e := range gqlErr.Errors {
if e.Type != "INSUFFICIENT_SCOPES" {
continue
}
missing.AddValues(requiredScopesFromServerMessage(e.Message))
}
if missing.Len() > 0 {
s := missing.ToSlice()
return fmt.Errorf(
"error: your authentication token is missing required scopes %v\n"+
"To request it, run: gh auth refresh -s %s",
s,
strings.Join(s, ","))
}
return nil
}

var scopesRE = regexp.MustCompile(`one of the following scopes: \[(.+?)]`)

func requiredScopesFromServerMessage(msg string) []string {
m := scopesRE.FindStringSubmatch(msg)
if m == nil {
return nil
}
var scopes []string
for _, mm := range strings.Split(m[1], ",") {
scopes = append(scopes, strings.Trim(mm, "' "))
}
return scopes
}

// ScopesSuggestion is an error messaging utility that prints the suggestion to request additional OAuth
// scopes in case a server response indicates that there are missing scopes.
func ScopesSuggestion(resp *http.Response) string {
Expand Down
77 changes: 0 additions & 77 deletions api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (

"github.com/cli/cli/v2/pkg/httpmock"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/go-gh/v2/pkg/api"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -257,79 +256,3 @@ func TestHTTPHeaders(t *testing.T) {
}
assert.Equal(t, "", stderr.String())
}

func TestGenerateScopeErrorForGQL(t *testing.T) {
tests := []struct {
name string
gqlError *api.GraphQLError
wantErr bool
expected string
}{
{
name: "missing scope",
gqlError: &api.GraphQLError{
Errors: []api.GraphQLErrorItem{
{
Type: "INSUFFICIENT_SCOPES",
Message: "The 'addProjectV2ItemById' field requires one of the following scopes: ['project']",
},
},
},
wantErr: true,
expected: "error: your authentication token is missing required scopes [project]\n" +
"To request it, run: gh auth refresh -s project",
},

{
name: "ignore non-scope errors",
gqlError: &api.GraphQLError{
Errors: []api.GraphQLErrorItem{
{
Type: "NOT_FOUND",
Message: "Could not resolve to a Repository",
},
},
},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := GenerateScopeErrorForGQL(tt.gqlError)
if tt.wantErr {
assert.NotNil(t, err)
assert.Equal(t, tt.expected, err.Error())
} else {
assert.Nil(t, err)
}
})
}
}

func TestRequiredScopesFromServerMessage(t *testing.T) {
tests := []struct {
msg string
expected []string
}{
{
msg: "requires one of the following scopes: ['project']",
expected: []string{"project"},
},
{
msg: "requires one of the following scopes: ['repo', 'read:org']",
expected: []string{"repo", "read:org"},
},
{
msg: "no match here",
expected: nil,
},
}

for _, tt := range tests {
t.Run(tt.msg, func(t *testing.T) {
output := requiredScopesFromServerMessage(tt.msg)
assert.Equal(t, tt.expected, output)
})
}
}
37 changes: 33 additions & 4 deletions pkg/cmd/project/shared/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"

"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/set"
"github.com/shurcooL/githubv4"
)

Expand Down Expand Up @@ -1662,15 +1664,42 @@ func (c *Client) UnlinkProjectFromTeam(projectID string, teamID string) error {
}

func handleError(err error) error {
var gqlErr api.GraphQLError
if errors.As(err, &gqlErr) {
if scopeErr := api.GenerateScopeErrorForGQL(gqlErr.GraphQLError); scopeErr != nil {
return scopeErr
var gerr api.GraphQLError
if errors.As(err, &gerr) {
missing := set.NewStringSet()
for _, e := range gerr.Errors {
if e.Type != "INSUFFICIENT_SCOPES" {
continue
}
missing.AddValues(requiredScopesFromServerMessage(e.Message))
}
if missing.Len() > 0 {
s := missing.ToSlice()
// TODO: this duplicates parts of generateScopesSuggestion
return fmt.Errorf(
"error: your authentication token is missing required scopes %v\n"+
"To request it, run: gh auth refresh -s %s",
s,
strings.Join(s, ","))
}
}
return err
}

var scopesRE = regexp.MustCompile(`one of the following scopes: \[(.+?)]`)

func requiredScopesFromServerMessage(msg string) []string {
m := scopesRE.FindStringSubmatch(msg)
if m == nil {
return nil
}
var scopes []string
for _, mm := range strings.Split(m[1], ",") {
scopes = append(scopes, strings.Trim(mm, "' "))
}
return scopes
}

func projectFieldValueData(v FieldValueNodes) interface{} {
switch v.Type {
case "ProjectV2ItemFieldDateValue":
Expand Down
32 changes: 32 additions & 0 deletions pkg/cmd/project/shared/queries/queries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package queries
import (
"io"
"net/http"
"reflect"
"strings"
"testing"

Expand Down Expand Up @@ -563,6 +564,37 @@ func TestProjectFields_NoLimit(t *testing.T) {
assert.Len(t, project.Fields.Nodes, 3)
}

func Test_requiredScopesFromServerMessage(t *testing.T) {
tests := []struct {
name string
msg string
want []string
}{
{
name: "no scopes",
msg: "SERVER OOPSIE",
want: []string(nil),
},
{
name: "one scope",
msg: "Your token has not been granted the required scopes to execute this query. The 'dataType' field requires one of the following scopes: ['read:project'], but your token has only been granted the: ['codespace', repo'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens.",
want: []string{"read:project"},
},
{
name: "multiple scopes",
msg: "Your token has not been granted the required scopes to execute this query. The 'dataType' field requires one of the following scopes: ['read:project', 'read:discussion', 'codespace'], but your token has only been granted the: [repo'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens.",
want: []string{"read:project", "read:discussion", "codespace"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := requiredScopesFromServerMessage(tt.msg); !reflect.DeepEqual(got, tt.want) {
t.Errorf("requiredScopesFromServerMessage() = %v, want %v", got, tt.want)
}
})
}
}

func TestNewProject_nonTTY(t *testing.T) {
client := NewTestClient()
_, err := client.NewProject(false, &Owner{}, 0, false)
Expand Down
Loading