From 5fddcef0a8f257257fa41fc0916405cca270c4e8 Mon Sep 17 00:00:00 2001 From: "Babak K. Shandiz" Date: Tue, 23 Sep 2025 15:24:37 +0100 Subject: [PATCH] fix(auth status): return JSON entries under `hosts` Signed-off-by: Babak K. Shandiz --- pkg/cmd/auth/status/auth_state.go | 28 ----- pkg/cmd/auth/status/status.go | 188 +++++++++++++++-------------- pkg/cmd/auth/status/status_test.go | 170 ++++++++++---------------- 3 files changed, 161 insertions(+), 225 deletions(-) delete mode 100644 pkg/cmd/auth/status/auth_state.go diff --git a/pkg/cmd/auth/status/auth_state.go b/pkg/cmd/auth/status/auth_state.go deleted file mode 100644 index 0c7e47675..000000000 --- a/pkg/cmd/auth/status/auth_state.go +++ /dev/null @@ -1,28 +0,0 @@ -package status - -import "encoding/json" - -type authState int - -const ( - authStateSuccess authState = iota - authStateTimeout - authStateError -) - -func (s authState) String() string { - switch s { - case authStateSuccess: - return "success" - case authStateTimeout: - return "timeout" - case authStateError: - return "error" - default: - return "unknown" - } -} - -func (s authState) MarshalJSON() ([]byte, error) { - return json.Marshal(s.String()) -} diff --git a/pkg/cmd/auth/status/status.go b/pkg/cmd/auth/status/status.go index 45f66d9c4..9869d9c79 100644 --- a/pkg/cmd/auth/status/status.go +++ b/pkg/cmd/auth/status/status.go @@ -19,40 +19,56 @@ import ( "github.com/spf13/cobra" ) +type authEntryState string + +const ( + authEntryStateSuccess = "success" + authEntryStateTimeout = "timeout" + authEntryStateError = "error" +) + type authEntry struct { - State authState `json:"state"` - Error string `json:"error"` - Active bool `json:"active"` - Host string `json:"host"` - Login string `json:"login"` - TokenSource string `json:"tokenSource"` - Token string `json:"token"` - Scopes string `json:"scopes"` - GitProtocol string `json:"gitProtocol"` + State authEntryState `json:"state"` + Error string `json:"error,omitempty"` + Active bool `json:"active"` + Host string `json:"host"` + Login string `json:"login"` + TokenSource string `json:"tokenSource"` + Token string `json:"token,omitempty"` + Scopes string `json:"scopes,omitempty"` + GitProtocol string `json:"gitProtocol"` } -var authFields = []string{ - "state", - "error", - "active", - "host", - "login", - "tokenSource", - "token", - "scopes", - "gitProtocol", +type authStatus struct { + Hosts map[string][]authEntry `json:"hosts"` } -func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { +func newAuthStatus() *authStatus { + return &authStatus{ + Hosts: make(map[string][]authEntry), + } +} + +var authStatusFields = []string{ + "hosts", +} + +func (a authStatus) ExportData(fields []string) map[string]interface{} { + return cmdutil.StructExportData(a, fields) +} + +func (e authEntry) String(cs *iostreams.ColorScheme) string { var sb strings.Builder - if e.State == authStateSuccess { + + switch e.State { + case authEntryStateSuccess: sb.WriteString( fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource), ) activeStr := fmt.Sprintf("%v", e.Active) sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol))) - sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(displayToken(e.Token, showToken)))) + sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.Token))) if expectScopes(e.Token) { sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes)))) @@ -68,18 +84,15 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { } } } - return sb.String() - } - if e.Login != "" { - sb.WriteString(fmt.Sprintf(" %s %s to %s account %s (%s)\n", cs.Red("X"), e.Error, e.Host, cs.Bold(e.Login), e.TokenSource)) - } else { - sb.WriteString(fmt.Sprintf(" %s %s to %s using token (%s)\n", cs.Red("X"), e.Error, e.Host, e.TokenSource)) - } - activeStr := fmt.Sprintf("%v", e.Active) - sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) - - if e.State == authStateError { + case authEntryStateError: + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource)) if authTokenWriteable(e.TokenSource) { loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host) @@ -87,12 +100,18 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string { sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions))) sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions))) } - } - return sb.String() -} -func (e authEntry) ExportData(fields []string) map[string]interface{} { - return cmdutil.StructExportData(e, fields) + case authEntryStateTimeout: + if e.Login != "" { + sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource)) + } else { + sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource)) + } + activeStr := fmt.Sprintf("%v", e.Active) + sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr))) + } + + return sb.String() } type StatusOptions struct { @@ -138,22 +157,15 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co $ gh auth status --show-token # Format authentication status as JSON - $ gh auth status --json active,login,host + $ gh auth status --json hosts # Include plain text token in JSON output - $ gh auth status --json token,login + $ gh auth status --json hosts --show-token - # Format output as a flat JSON array - $ gh auth status --json active,host,login --jq add + # Format hosts as a flat JSON array + $ gh auth status --json hosts --jq '.hosts | add' `), RunE: func(cmd *cobra.Command, args []string) error { - if err := cmdutil.MutuallyExclusive( - "`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.", - opts.Exporter != nil, - opts.ShowToken, - ); err != nil { - return err - } if runF != nil { return runF(opts) } @@ -167,7 +179,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only") // the json flags are intentionally not given a shorthand to avoid conflict with -t/--show-token - cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authFields) + cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authStatusFields) return cmd } @@ -183,20 +195,13 @@ func statusRun(opts *StatusOptions) error { stdout := opts.IO.Out cs := opts.IO.ColorScheme() - showToken := opts.ShowToken - if opts.Exporter != nil && slices.Contains(opts.Exporter.Fields(), "token") { - showToken = true - } - - statuses := make(map[string][]authEntry) - hostnames := authCfg.Hosts() if len(hostnames) == 0 { fmt.Fprintf(stderr, "You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login")) if opts.Exporter != nil { // In machine-friendly mode, we always exit with no error. - opts.Exporter.Write(opts.IO, struct{}{}) + opts.Exporter.Write(opts.IO, newAuthStatus()) return nil } return cmdutil.SilentError @@ -207,7 +212,7 @@ func statusRun(opts *StatusOptions) error { "You are not logged into any accounts on %s\n", opts.Hostname) if opts.Exporter != nil { // In machine-friendly mode, we always exit with no error. - opts.Exporter.Write(opts.IO, struct{}{}) + opts.Exporter.Write(opts.IO, newAuthStatus()) return nil } return cmdutil.SilentError @@ -218,6 +223,9 @@ func statusRun(opts *StatusOptions) error { return err } + var finalErr error + statuses := newAuthStatus() + for _, hostname := range hostnames { if opts.Hostname != "" && opts.Hostname != hostname { continue @@ -233,15 +241,14 @@ func statusRun(opts *StatusOptions) error { active: true, gitProtocol: gitProtocol, hostname: hostname, - showToken: showToken, token: activeUserToken, tokenSource: activeUserTokenSource, username: activeUser, }) - statuses[hostname] = append(statuses[hostname], entry) + statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry) - if err == nil && !isValidEntry(entry) { - err = cmdutil.SilentError + if finalErr == nil && entry.State != authEntryStateSuccess { + finalErr = cmdutil.SilentError } if opts.Active { @@ -258,33 +265,46 @@ func statusRun(opts *StatusOptions) error { active: false, gitProtocol: gitProtocol, hostname: hostname, - showToken: showToken, token: token, tokenSource: tokenSource, username: username, }) - statuses[hostname] = append(statuses[hostname], entry) + statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry) - if err == nil && !isValidEntry(entry) { - err = cmdutil.SilentError + if finalErr == nil && entry.State != authEntryStateSuccess { + finalErr = cmdutil.SilentError + } + } + } + + if !opts.ShowToken { + for _, host := range statuses.Hosts { + for i := range host { + if opts.Exporter != nil { + // In machine-readable we just drop the token + host[i].Token = "" + } else { + host[i].Token = maskToken(host[i].Token) + } } } } if opts.Exporter != nil { + // In machine-friendly mode, we always exit with no error. opts.Exporter.Write(opts.IO, statuses) return nil } prevEntry := false for _, hostname := range hostnames { - entries, ok := statuses[hostname] + entries, ok := statuses.Hosts[hostname] if !ok { continue } stream := stdout - if err != nil { + if finalErr != nil { stream = stderr } @@ -294,26 +314,21 @@ func statusRun(opts *StatusOptions) error { prevEntry = true fmt.Fprintf(stream, "%s\n", cs.Bold(hostname)) for i, entry := range entries { - fmt.Fprintf(stream, "%s", entry.String(cs, showToken)) + fmt.Fprintf(stream, "%s", entry.String(cs)) if i < len(entries)-1 { fmt.Fprint(stream, "\n") } } } - return err + return finalErr } -func displayToken(token string, printRaw bool) string { - if printRaw { - return token - } - +func maskToken(token string) string { if idx := strings.LastIndexByte(token, '_'); idx > -1 { prefix := token[0 : idx+1] return prefix + strings.Repeat("*", len(token)-len(prefix)) } - return strings.Repeat("*", len(token)) } @@ -336,7 +351,6 @@ type buildEntryOptions struct { active bool gitProtocol string hostname string - showToken bool token string tokenSource string username string @@ -367,8 +381,8 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { var err error entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname) if err != nil { - entry.State = authStateError - entry.Error = "Failed to log in" + entry.State = authEntryStateError + entry.Error = err.Error() return entry } } @@ -378,25 +392,21 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry { if err != nil { var networkError net.Error if errors.As(err, &networkError) && networkError.Timeout() { - entry.State = authStateTimeout - entry.Error = "Timeout trying to log in" + entry.State = authEntryStateTimeout + entry.Error = err.Error() return entry } - entry.State = authStateError - entry.Error = "Failed to log in" + entry.State = authEntryStateError + entry.Error = err.Error() return entry } entry.Scopes = scopesHeader - entry.State = authStateSuccess + entry.State = authEntryStateSuccess return entry } func authTokenWriteable(src string) bool { return !strings.HasSuffix(src, "_TOKEN") } - -func isValidEntry(entry authEntry) bool { - return entry.State == authStateSuccess -} diff --git a/pkg/cmd/auth/status/status_test.go b/pkg/cmd/auth/status/status_test.go index 300ed0043..71b7ea4e2 100644 --- a/pkg/cmd/auth/status/status_test.go +++ b/pkg/cmd/auth/status/status_test.go @@ -22,10 +22,9 @@ import ( func Test_NewCmdStatus(t *testing.T) { tests := []struct { - name string - cli string - wants StatusOptions - wantErr error + name string + cli string + wants StatusOptions }{ { name: "no arguments", @@ -53,11 +52,6 @@ func Test_NewCmdStatus(t *testing.T) { Active: true, }, }, - { - name: "both --show-token and --json flags", - cli: "--show-token --json state,token", - wantErr: cmdutil.FlagErrorf("`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`."), - }, } for _, tt := range tests { @@ -82,30 +76,18 @@ func Test_NewCmdStatus(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) _, err = cmd.ExecuteC() - if tt.wantErr == nil { - assert.NoError(t, err) + assert.NoError(t, err) - assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) - assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) - assert.Equal(t, tt.wants.Active, gotOpts.Active) - } else { - assert.Equal(t, tt.wantErr, err) - } + assert.Equal(t, tt.wants.Hostname, gotOpts.Hostname) + assert.Equal(t, tt.wants.ShowToken, gotOpts.ShowToken) + assert.Equal(t, tt.wants.Active, gotOpts.Active) }) } } func TestJSONFields(t *testing.T) { jsonfieldstest.ExpectCommandToSupportJSONFields(t, NewCmdStatus, []string{ - "state", - "error", - "active", - "host", - "login", - "tokenSource", - "token", - "scopes", - "gitProtocol", + "hosts", }) } @@ -113,6 +95,7 @@ func Test_statusRun(t *testing.T) { tests := []struct { name string opts StatusOptions + jsonFields []string env map[string]string httpStubs func(*httpmock.Registry) cfgStubs func(*testing.T, gh.Config) @@ -556,30 +539,30 @@ func Test_statusRun(t *testing.T) { `), }, { - name: "No tokens with json flag", - opts: StatusOptions{ - Exporter: defaultJsonExporter(), - }, - wantOut: "{}\n", + name: "json, no tokens", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, + wantOut: "{\"hosts\":{}}\n", wantErrOut: "You are not logged into any GitHub hosts. To log in, run: gh auth login\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "No token for the given --hostname with json flag", + name: "json, no token for given --hostname", opts: StatusOptions{ Hostname: "foo.com", - Exporter: defaultJsonExporter(), }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") }, - wantOut: "{}\n", + wantOut: "{\"hosts\":{}}\n", wantErrOut: "You are not logged into any accounts on foo.com\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "All valid tokens with json flag", - opts: StatusOptions{ - Exporter: defaultJsonExporter(), - }, + name: "json, all valid tokens", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") login(t, c, "github.com", "monalisa2", "gho_abc123", "https") @@ -598,23 +581,15 @@ func Test_statusRun(t *testing.T) { reg.Register( httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) - }, - wantOut: `{` + - `"ghe.io":[` + - `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + - `],` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + - `{"active":false,"host":"github.com","login":"monalisa","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"ghe.io":[{"state":"success","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}],"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"},{"state":"success","active":false,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, { - name: "All valid tokens with hostname and json flag", + name: "json, all valid tokens with hostname", opts: StatusOptions{ Hostname: "github.com", - Exporter: defaultJsonExporter(), }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") login(t, c, "github.com", "monalisa2", "gho_abc123", "https") @@ -629,18 +604,14 @@ func Test_statusRun(t *testing.T) { httpmock.REST("GET", ""), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) }, - wantOut: `{` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa2","state":"success"},` + - `{"active":false,"host":"github.com","login":"monalisa","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"},{"state":"success","active":false,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, { - name: "All valid tokens with active and json flag", + name: "json, all valid tokens with active", opts: StatusOptions{ - Active: true, - Exporter: defaultJsonExporter(), + Active: true, }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "gho_abc123", "https") login(t, c, "github.com", "monalisa2", "gho_abc123", "https") @@ -655,20 +626,13 @@ func Test_statusRun(t *testing.T) { httpmock.REST("GET", "api/v3/"), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) }, - wantOut: `{` + - `"ghe.io":[` + - `{"active":true,"host":"ghe.io","login":"monalisa-ghe","state":"success"}` + - `],` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa2","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"ghe.io":[{"state":"success","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}],"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa2","tokenSource":"GH_CONFIG_DIR/hosts.yml","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, { - name: "token from env with json flag", - opts: StatusOptions{ - Exporter: defaultJsonExporter(), - }, - env: map[string]string{"GH_TOKEN": "gho_abc123"}, + name: "json, token from env", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, + env: map[string]string{"GH_TOKEN": "gho_abc123"}, httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.REST("GET", ""), @@ -677,16 +641,12 @@ func Test_statusRun(t *testing.T) { httpmock.GraphQL(`query UserCurrent\b`), httpmock.StringResponse(`{"data":{"viewer":{"login":"monalisa"}}}`)) }, - wantOut: `{` + - `"github.com":[` + - `{"active":true,"host":"github.com","login":"monalisa","state":"success"}` + - "]}\n", + wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n", }, { - name: "bad token with json flag", - opts: StatusOptions{ - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), - }, + name: "json, bad token", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "ghe.io", "monalisa-ghe", "gho_abc123", "https") }, @@ -694,28 +654,29 @@ func Test_statusRun(t *testing.T) { // mock for HeaderHasMinimumScopes api requests to a non-github.com host reg.Register(httpmock.REST("GET", "api/v3/"), httpmock.StatusStringResponse(400, "no bueno")) }, - wantOut: `{"ghe.io":[{"active":true,"error":"Failed to log in","host":"ghe.io","login":"monalisa-ghe","state":"error"}]}` + "\n", + wantOut: `{"hosts":{"ghe.io":[{"state":"error","error":"HTTP 400 (https://ghe.io/api/v3/)","active":true,"host":"ghe.io","login":"monalisa-ghe","tokenSource":"GH_CONFIG_DIR/hosts.yml","gitProtocol":"https"}]}}` + "\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "bad token from env with json flag", - opts: StatusOptions{ - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), - }, - env: map[string]string{"GH_TOKEN": "gho_abc123"}, + name: "json, bad token from env", + opts: StatusOptions{}, + jsonFields: []string{"hosts"}, + env: map[string]string{"GH_TOKEN": "gho_abc123"}, httpStubs: func(reg *httpmock.Registry) { // mock for HeaderHasMinimumScopes api requests to a non-github.com host reg.Register( httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StatusStringResponse(400, "no bueno")) + httpmock.StatusStringResponse(400, `no bueno`)) }, - wantOut: `{"github.com":[{"active":true,"error":"Failed to log in","host":"github.com","login":"","state":"error"}]}` + "\n", + wantOut: `{"hosts":{"github.com":[{"state":"error","error":"non-200 OK status code: body: \"no bueno\"","active":true,"host":"github.com","login":"","tokenSource":"GH_TOKEN","gitProtocol":"https"}]}}` + "\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "timeout error with json flag", + name: "json, timeout error", opts: StatusOptions{ Hostname: "github.com", - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"error"}), }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") }, @@ -725,14 +686,16 @@ func Test_statusRun(t *testing.T) { return nil, context.DeadlineExceeded }) }, - wantOut: `{"github.com":[{"active":true,"error":"Timeout trying to log in","host":"github.com","login":"monalisa","state":"timeout"}]}` + "\n", + wantOut: `{"hosts":{"github.com":[{"state":"timeout","error":"Get \"https://api.github.com/\": context deadline exceeded","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","gitProtocol":"https"}]}}` + "\n", + wantErr: nil, // should not return error in machine-readable mode }, { - name: "token is not masked with json flag", + name: "json, with show token", opts: StatusOptions{ - Hostname: "github.com", - Exporter: addFieldsToExporter(defaultJsonExporter(), []string{"token"}), + Hostname: "github.com", + ShowToken: true, }, + jsonFields: []string{"hosts"}, cfgStubs: func(t *testing.T, c gh.Config) { login(t, c, "github.com", "monalisa", "abc123", "https") }, @@ -742,18 +705,18 @@ func Test_statusRun(t *testing.T) { httpmock.REST("GET", ""), httpmock.WithHeader(httpmock.ScopesResponder("repo,read:org"), "X-Oauth-Scopes", "repo, read:org")) }, - wantOut: `{"github.com":[{"active":true,"host":"github.com","login":"monalisa","state":"success","token":"abc123"}]}` + "\n", + wantOut: `{"hosts":{"github.com":[{"state":"success","active":true,"host":"github.com","login":"monalisa","tokenSource":"GH_CONFIG_DIR/hosts.yml","token":"abc123","scopes":"repo, read:org","gitProtocol":"https"}]}}` + "\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ios, _, stdout, stderr := iostreams.Test() - ios.SetStdinTTY(true) ios.SetStderrTTY(true) ios.SetStdoutTTY(true) tt.opts.IO = ios + cfg, _ := config.NewIsolatedTestConfig(t) if tt.cfgStubs != nil { tt.cfgStubs(t, cfg) @@ -771,6 +734,12 @@ func Test_statusRun(t *testing.T) { tt.httpStubs(reg) } + if tt.jsonFields != nil { + jsonExporter := cmdutil.NewJSONExporter() + jsonExporter.SetFields(tt.jsonFields) + tt.opts.Exporter = jsonExporter + } + for k, v := range tt.env { t.Setenv(k, v) } @@ -795,18 +764,3 @@ func login(t *testing.T, c gh.Config, hostname, username, token, protocol string _, err := c.Authentication().Login(hostname, username, token, protocol, false) require.NoError(t, err) } - -type exporter interface { - cmdutil.Exporter - SetFields(fields []string) -} - -func addFieldsToExporter(e exporter, fields []string) exporter { - newFields := append(e.Fields(), fields...) - e.SetFields(newFields) - return e -} -func defaultJsonExporter() exporter { - return addFieldsToExporter(cmdutil.NewJSONExporter(), []string{"login", "host", "state", "active"}) - -}