From ec25b735ab2d2004ed76bab5a273205d93093d14 Mon Sep 17 00:00:00 2001 From: vilmibm Date: Thu, 6 Aug 2020 12:09:25 -0500 Subject: [PATCH] gh auth status --- api/client.go | 7 +- command/root.go | 2 + pkg/cmd/auth/auth.go | 7 - pkg/cmd/auth/{login => client}/client.go | 8 +- pkg/cmd/auth/login/login.go | 11 +- pkg/cmd/auth/login/login_test.go | 45 ++-- pkg/cmd/auth/status/status.go | 152 ++++++++++++ pkg/cmd/auth/status/status_test.go | 294 +++++++++++++++++++++++ pkg/httpmock/stub.go | 13 + 9 files changed, 492 insertions(+), 47 deletions(-) rename pkg/cmd/auth/{login => client}/client.go (81%) create mode 100644 pkg/cmd/auth/status/status.go create mode 100644 pkg/cmd/auth/status/status_test.go diff --git a/api/client.go b/api/client.go index 233d460f0..2058eb44b 100644 --- a/api/client.go +++ b/api/client.go @@ -196,6 +196,10 @@ func (err HTTPError) Error() string { return fmt.Sprintf("HTTP %d (%s)", err.StatusCode, err.RequestURL) } +type MissingScopesError struct { + error +} + func (c Client) HasMinimumScopes(hostname string) (bool, error) { apiEndpoint := ghinstance.RESTPrefix(hostname) @@ -243,11 +247,10 @@ func (c Client) HasMinimumScopes(hostname string) (bool, error) { } if len(errorMsgs) > 0 { - return false, errors.New(strings.Join(errorMsgs, ";")) + return false, &MissingScopesError{error: errors.New(strings.Join(errorMsgs, ";"))} } return true, nil - } // GraphQL performs a GraphQL request and parses the response diff --git a/command/root.go b/command/root.go index 9eb1c9287..52939b403 100644 --- a/command/root.go +++ b/command/root.go @@ -25,6 +25,7 @@ import ( authCmd "github.com/cli/cli/pkg/cmd/auth" authLoginCmd "github.com/cli/cli/pkg/cmd/auth/login" authLogoutCmd "github.com/cli/cli/pkg/cmd/auth/logout" + authStatusCmd "github.com/cli/cli/pkg/cmd/auth/status" gistCreateCmd "github.com/cli/cli/pkg/cmd/gist/create" prCmd "github.com/cli/cli/pkg/cmd/pr" repoCmd "github.com/cli/cli/pkg/cmd/repo" @@ -138,6 +139,7 @@ func init() { RootCmd.AddCommand(authCmd.Cmd) authCmd.Cmd.AddCommand(authLoginCmd.NewCmdLogin(cmdFactory, nil)) authCmd.Cmd.AddCommand(authLogoutCmd.NewCmdLogout(cmdFactory, nil)) + authCmd.Cmd.AddCommand(authStatusCmd.NewCmdStatus(cmdFactory, nil)) resolvedBaseRepo := func() (ghrepo.Interface, error) { httpClient, err := cmdFactory.HttpClient() diff --git a/pkg/cmd/auth/auth.go b/pkg/cmd/auth/auth.go index ff1f79c7c..f13928234 100644 --- a/pkg/cmd/auth/auth.go +++ b/pkg/cmd/auth/auth.go @@ -8,11 +8,4 @@ var Cmd = &cobra.Command{ Use: "auth ", Short: "Login, logout, and refresh your authentication", Long: `Manage gh's authentication state.`, - // TODO this all doesn't exist yet - //Example: heredoc.Doc(` - // $ gh auth login - // $ gh auth status - // $ gh auth refresh --scopes gist - // $ gh auth logout - //`), } diff --git a/pkg/cmd/auth/login/client.go b/pkg/cmd/auth/client/client.go similarity index 81% rename from pkg/cmd/auth/login/client.go rename to pkg/cmd/auth/client/client.go index e133ca5c8..a3652e45d 100644 --- a/pkg/cmd/auth/login/client.go +++ b/pkg/cmd/auth/client/client.go @@ -1,4 +1,4 @@ -package login +package client import ( "fmt" @@ -8,8 +8,8 @@ import ( "github.com/cli/cli/internal/config" ) -func validateHostCfg(hostname string, cfg config.Config) error { - apiClient, err := clientFromCfg(hostname, cfg) +func ValidateHostCfg(hostname string, cfg config.Config) error { + apiClient, err := ClientFromCfg(hostname, cfg) if err != nil { return err } @@ -22,7 +22,7 @@ func validateHostCfg(hostname string, cfg config.Config) error { return nil } -var clientFromCfg = func(hostname string, cfg config.Config) (*api.Client, error) { +var ClientFromCfg = func(hostname string, cfg config.Config) (*api.Client, error) { var opts []api.ClientOption token, err := cfg.Get(hostname, "oauth_token") diff --git a/pkg/cmd/auth/login/login.go b/pkg/cmd/auth/login/login.go index 72710f5d0..cd43c536c 100644 --- a/pkg/cmd/auth/login/login.go +++ b/pkg/cmd/auth/login/login.go @@ -12,6 +12,7 @@ import ( "github.com/cli/cli/api" "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmd/auth/client" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/iostreams" "github.com/cli/cli/pkg/prompt" @@ -119,7 +120,7 @@ func loginRun(opts *LoginOptions) error { return err } - err = validateHostCfg(opts.Hostname, cfg) + err = client.ValidateHostCfg(opts.Hostname, cfg) if err != nil { return err } @@ -167,9 +168,9 @@ func loginRun(opts *LoginOptions) error { existingToken, _ := cfg.Get(hostname, "oauth_token") if existingToken != "" { - err := validateHostCfg(hostname, cfg) + err := client.ValidateHostCfg(hostname, cfg) if err == nil { - apiClient, err := clientFromCfg(hostname, cfg) + apiClient, err := client.ClientFromCfg(hostname, cfg) if err != nil { return err } @@ -235,7 +236,7 @@ func loginRun(opts *LoginOptions) error { return err } - err = validateHostCfg(hostname, cfg) + err = client.ValidateHostCfg(hostname, cfg) if err != nil { return err } @@ -263,7 +264,7 @@ func loginRun(opts *LoginOptions) error { fmt.Fprintf(opts.IO.ErrOut, "%s Configured git protocol\n", utils.GreenCheck()) - apiClient, err := clientFromCfg(hostname, cfg) + apiClient, err := client.ClientFromCfg(hostname, cfg) if err != nil { return err } diff --git a/pkg/cmd/auth/login/login_test.go b/pkg/cmd/auth/login/login_test.go index 4cc818670..4af891b66 100644 --- a/pkg/cmd/auth/login/login_test.go +++ b/pkg/cmd/auth/login/login_test.go @@ -2,7 +2,6 @@ package login import ( "bytes" - "io/ioutil" "net/http" "os" "regexp" @@ -10,6 +9,7 @@ import ( "github.com/cli/cli/api" "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmd/auth/client" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/httpmock" "github.com/cli/cli/pkg/iostreams" @@ -164,19 +164,6 @@ func Test_NewCmdLogin(t *testing.T) { } } -func scopesResponder(scopes string) func(*http.Request) (*http.Response, error) { - return func(req *http.Request) (*http.Response, error) { - return &http.Response{ - StatusCode: 200, - Request: req, - Header: map[string][]string{ - "X-Oauth-Scopes": {scopes}, - }, - Body: ioutil.NopCloser(bytes.NewBufferString("")), - }, nil - } -} - func Test_loginRun_nontty(t *testing.T) { tests := []struct { name string @@ -200,7 +187,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc123", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), scopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org")) }, wantHosts: "albert.wesker:\n oauth_token: abc123\n", }, @@ -211,7 +198,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc456", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), scopesResponder("read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("read:org")) }, wantErr: regexp.MustCompile(`missing required scope 'repo'`), }, @@ -222,7 +209,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc456", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), scopesResponder("repo")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo")) }, wantErr: regexp.MustCompile(`missing required scope 'read:org'`), }, @@ -233,7 +220,7 @@ func Test_loginRun_nontty(t *testing.T) { Token: "abc456", }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), scopesResponder("repo,admin:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,admin:org")) }, wantHosts: "github.com:\n oauth_token: abc456\n", }, @@ -252,11 +239,11 @@ func Test_loginRun_nontty(t *testing.T) { tt.opts.IO = io t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} - origClientFromCfg := clientFromCfg + origClientFromCfg := client.ClientFromCfg defer func() { - clientFromCfg = origClientFromCfg + client.ClientFromCfg = origClientFromCfg }() - clientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { + client.ClientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { httpClient := &http.Client{Transport: reg} return api.NewClientFromHTTP(httpClient), nil } @@ -264,7 +251,7 @@ func Test_loginRun_nontty(t *testing.T) { if tt.httpStubs != nil { tt.httpStubs(reg) } else { - reg.Register(httpmock.REST("GET", ""), scopesResponder("repo,read:org")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org")) } mainBuf := bytes.Buffer{} @@ -305,7 +292,7 @@ func Test_loginRun_Survey(t *testing.T) { _ = cfg.Set("github.com", "oauth_token", "ghi789") }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", ""), scopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -328,7 +315,7 @@ func Test_loginRun_Survey(t *testing.T) { as.StubOne("HTTPS") // git_protocol }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), scopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -345,7 +332,7 @@ func Test_loginRun_Survey(t *testing.T) { as.StubOne("HTTPS") // git_protocol }, httpStubs: func(reg *httpmock.Registry) { - reg.Register(httpmock.REST("GET", "api/v3/"), scopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) @@ -397,18 +384,18 @@ func Test_loginRun_Survey(t *testing.T) { t.Run(tt.name, func(t *testing.T) { reg := &httpmock.Registry{} - origClientFromCfg := clientFromCfg + origClientFromCfg := client.ClientFromCfg defer func() { - clientFromCfg = origClientFromCfg + client.ClientFromCfg = origClientFromCfg }() - clientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { + client.ClientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { httpClient := &http.Client{Transport: reg} return api.NewClientFromHTTP(httpClient), nil } if tt.httpStubs != nil { tt.httpStubs(reg) } else { - reg.Register(httpmock.REST("GET", ""), scopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) reg.Register( httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"jillv"}}}`)) diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go new file mode 100644 index 000000000..618fa7c4d --- /dev/null +++ b/pkg/cmd/auth/status/status.go @@ -0,0 +1,152 @@ +package status + +import ( + "errors" + "fmt" + "net/http" + "os" + + "github.com/MakeNowJust/heredoc" + "github.com/cli/cli/api" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/internal/ghinstance" + "github.com/cli/cli/pkg/cmd/auth/client" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/cli/cli/utils" + "github.com/spf13/cobra" +) + +type StatusOptions struct { + HttpClient func() (*http.Client, error) + IO *iostreams.IOStreams + Config func() (config.Config, error) + Token string + Hostname string +} + +func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Command { + opts := &StatusOptions{ + HttpClient: f.HttpClient, + IO: f.IOStreams, + Config: f.Config, + } + + cmd := &cobra.Command{ + Use: "status", + Args: cobra.ExactArgs(0), + Short: "View authentication status", + Long: heredoc.Doc(`Verifies and displays information about your authentication state. + + This command will test your authentication state for each GitHub host that gh knows about and + report on any issues. + `), + RunE: func(cmd *cobra.Command, args []string) error { + // TODO support other names + opts.Token = os.Getenv("GITHUB_TOKEN") + + if opts.Token != "" && opts.Hostname == "" { + opts.Hostname = ghinstance.Default() + } + + if runF != nil { + return runF(opts) + } + + return statusRun(opts) + }, + } + + cmd.Flags().StringVarP(&opts.Hostname, "hostname", "h", "", "Check a specific hostname's auth status") + + return cmd +} + +func statusRun(opts *StatusOptions) error { + cfg, err := opts.Config() + if err != nil { + return err + } + + // TODO check tty + + stderr := opts.IO.ErrOut + + if opts.Token != "" { + hostname := opts.Hostname + err := cfg.Set(opts.Hostname, "oauth_token", opts.Token) + if err != nil { + return err + } + + apiClient, err := client.ClientFromCfg(hostname, cfg) + if err != nil { + return err + } + + _, err = apiClient.HasMinimumScopes(hostname) + if err != nil { + var missingScopes *api.MissingScopesError + if errors.As(err, &missingScopes) { + return fmt.Errorf("%s %s: %s", utils.Red("X"), hostname, err) + } else { + return fmt.Errorf("%s %s: authentication failed", utils.Red("X"), hostname) + } + } else { + username, err := api.CurrentLoginName(apiClient, hostname) + if err != nil { + return fmt.Errorf("%s %s: api call failed: %s\n", utils.Red("X"), hostname, err) + } + fmt.Fprintf(stderr, "%s token valid for %s as %s\n", utils.GreenCheck(), hostname, utils.Bold(username)) + } + + return nil + } + + hostnames, err := cfg.Hosts() + if len(hostnames) == 0 || err != nil { + fmt.Fprintf(stderr, "You are not logged into any GitHub hosts. Run 'gh auth login' to authenticate.\n") + return nil + } + + httpClient, err := opts.HttpClient() + if err != nil { + return err + } + apiClient := api.NewClientFromHTTP(httpClient) + + var failed bool + + for _, hostname := range hostnames { + if opts.Hostname != "" && opts.Hostname != hostname { + continue + } + _, err = apiClient.HasMinimumScopes(hostname) + if err != nil { + var missingScopes *api.MissingScopesError + if errors.As(err, &missingScopes) { + fmt.Fprintf(stderr, "%s %s: %s\n", utils.Red("X"), hostname, err) + } else { + fmt.Fprintf(stderr, "%s %s: authentication failed\n", utils.Red("X"), hostname) + } + failed = true + } else { + username, err := api.CurrentLoginName(apiClient, hostname) + if err != nil { + fmt.Fprintf(stderr, "%s %s: api call failed: %s\n", utils.Red("X"), hostname, err) + } + fmt.Fprintf(stderr, "%s Logged in to %s as %s\n", utils.GreenCheck(), hostname, utils.Bold(username)) + } + + // NB we could take this opportunity to add or fix the "user" key in the hosts config. I chose + // not to since I wanted this command to be read-only. + } + + if failed { + // TODO unsure about this; want non-zero exit but don't need to print anything more. Is the + // non-zero exit worth it? Should we tweak error handling to not print "" errors? + return errors.New("") + } + + return nil +} diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go new file mode 100644 index 000000000..df12350af --- /dev/null +++ b/pkg/cmd/auth/status/status_test.go @@ -0,0 +1,294 @@ +package status + +import ( + "bytes" + "net/http" + "os" + "regexp" + "testing" + + "github.com/cli/cli/api" + "github.com/cli/cli/internal/config" + "github.com/cli/cli/pkg/cmd/auth/client" + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/httpmock" + "github.com/cli/cli/pkg/iostreams" + "github.com/google/shlex" + "github.com/stretchr/testify/assert" +) + +func Test_NewCmdStatus(t *testing.T) { + tests := []struct { + name string + cli string + wants StatusOptions + ghtoken string + }{ + { + name: "ghtoken set", + cli: "", + wants: StatusOptions{ + Token: "abc123", + Hostname: "github.com", + }, + ghtoken: "abc123", + }, + { + name: "ghtoken set", + cli: "--hostname joel.miller", + wants: StatusOptions{ + Token: "def456", + Hostname: "joel.miller", + }, + ghtoken: "def456", + }, + { + name: "no arguments", + cli: "", + wants: StatusOptions{}, + }, + { + name: "hostname set", + cli: "--hostname ellie.williams", + wants: StatusOptions{ + Hostname: "ellie.williams", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ghtoken := os.Getenv("GITHUB_TOKEN") + defer func() { + os.Setenv("GITHUB_TOKEN", ghtoken) + }() + os.Setenv("GITHUB_TOKEN", tt.ghtoken) + + f := &cmdutil.Factory{} + + argv, err := shlex.Split(tt.cli) + assert.NoError(t, err) + + var gotOpts *StatusOptions + cmd := NewCmdStatus(f, func(opts *StatusOptions) error { + gotOpts = opts + return nil + }) + + // TODO cobra hack-around + cmd.Flags().BoolP("help", "x", false, "") + + cmd.SetArgs(argv) + cmd.SetIn(&bytes.Buffer{}) + cmd.SetOut(&bytes.Buffer{}) + cmd.SetErr(&bytes.Buffer{}) + + _, err = cmd.ExecuteC() + assert.NoError(t, err) + + assert.Equal(t, tt.wants.Token, gotOpts.Token) + assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + }) + } +} + +func Test_statusRun(t *testing.T) { + tests := []struct { + name string + opts *StatusOptions + httpStubs func(*httpmock.Registry) + cfg func(config.Config) + wantErr *regexp.Regexp + wantErrOut *regexp.Regexp + }{ + { + name: "token set, bad token", + opts: &StatusOptions{ + Token: "abc123", + Hostname: "github.com", + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.REST("GET", ""), + httpmock.StatusStringResponse(400, "no bueno"), + ) + }, + wantErr: regexp.MustCompile(`authentication failed`), + }, + { + name: "token set, missing scope", + opts: &StatusOptions{ + Token: "abc123", + Hostname: "github.com", + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,")) + }, + wantErr: regexp.MustCompile(`missing required scope 'read:org'`), + }, + { + name: "token set, good token", + opts: &StatusOptions{ + Token: "abc123", + Hostname: "github.com", + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + }, + wantErrOut: regexp.MustCompile(`token valid for github.com as.*tess`), + }, + { + name: "hostname set", + opts: &StatusOptions{ + Hostname: "joel.miller", + }, + cfg: func(c config.Config) { + _ = c.Set("joel.miller", "oauth_token", "abc123") + _ = c.Set("github.com", "oauth_token", "abc123") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + }, + wantErrOut: regexp.MustCompile(`Logged in to joel.miller as.*tess`), + }, + { + name: "hostname set", + opts: &StatusOptions{ + Hostname: "joel.miller", + }, + cfg: func(c config.Config) { + _ = c.Set("joel.miller", "oauth_token", "abc123") + _ = c.Set("github.com", "oauth_token", "abc123") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + }, + wantErrOut: regexp.MustCompile(`Logged in to joel.miller as.*tess`), + }, + { + name: "missing scope", + opts: &StatusOptions{}, + cfg: func(c config.Config) { + _ = c.Set("joel.miller", "oauth_token", "abc123") + _ = c.Set("github.com", "oauth_token", "abc123") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + }, + wantErrOut: regexp.MustCompile(`joel.miller: missing required.*Logged in to github.com as.*tess`), + wantErr: regexp.MustCompile(``), + }, + { + name: "bad token", + opts: &StatusOptions{}, + cfg: func(c config.Config) { + _ = c.Set("joel.miller", "oauth_token", "abc123") + _ = c.Set("github.com", "oauth_token", "abc123") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + }, + wantErrOut: regexp.MustCompile(`joel.miller: authentication failed.*Logged in to github.com as.*tess`), + wantErr: regexp.MustCompile(``), + }, + { + name: "all good", + opts: &StatusOptions{}, + cfg: func(c config.Config) { + _ = c.Set("joel.miller", "oauth_token", "abc123") + _ = c.Set("github.com", "oauth_token", "abc123") + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.ScopesResponder("repo,read:org,")) + reg.Register(httpmock.REST("GET", ""), httpmock.ScopesResponder("repo,read:org,")) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data":{"viewer":{"login":"tess"}}}`)) + }, + wantErrOut: regexp.MustCompile(`(?s)Logged in to github.com as.*tess.*Logged in to joel.miller as.*tess`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.opts == nil { + tt.opts = &StatusOptions{} + } + + io, _, _, stderr := iostreams.Test() + + io.SetStdinTTY(true) + io.SetStderrTTY(true) + io.SetStdoutTTY(true) + + tt.opts.IO = io + + cfg := config.NewBlankConfig() + + if tt.cfg != nil { + tt.cfg(cfg) + } + tt.opts.Config = func() (config.Config, error) { + return cfg, nil + } + + reg := &httpmock.Registry{} + origClientFromCfg := client.ClientFromCfg + defer func() { + client.ClientFromCfg = origClientFromCfg + }() + client.ClientFromCfg = func(_ string, _ config.Config) (*api.Client, error) { + httpClient := &http.Client{Transport: reg} + return api.NewClientFromHTTP(httpClient), nil + } + tt.opts.HttpClient = func() (*http.Client, error) { + return &http.Client{Transport: reg}, nil + } + if tt.httpStubs != nil { + tt.httpStubs(reg) + } + mainBuf := bytes.Buffer{} + hostsBuf := bytes.Buffer{} + defer config.StubWriteConfig(&mainBuf, &hostsBuf)() + + err := statusRun(tt.opts) + assert.Equal(t, tt.wantErr == nil, err == nil) + if err != nil { + if tt.wantErr != nil { + assert.True(t, tt.wantErr.MatchString(err.Error())) + return + } else { + t.Fatalf("unexpected error: %s", err) + } + } + + if tt.wantErrOut == nil { + assert.Equal(t, "", stderr.String()) + } else { + assert.True(t, tt.wantErrOut.MatchString(stderr.String())) + } + + reg.Verify(t) + }) + } +} diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 5f4ca58ec..a1dcefaa3 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -133,6 +133,19 @@ func GraphQLQuery(body string, cb func(string, map[string]interface{})) Responde } } +func ScopesResponder(scopes string) func(*http.Request) (*http.Response, error) { + return func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + Request: req, + Header: map[string][]string{ + "X-Oauth-Scopes": {scopes}, + }, + Body: ioutil.NopCloser(bytes.NewBufferString("")), + }, nil + } +} + func httpResponse(status int, req *http.Request, body io.Reader) *http.Response { return &http.Response{ StatusCode: status,