fix(auth status): return JSON entries under hosts

Signed-off-by: Babak K. Shandiz <babakks@github.com>
This commit is contained in:
Babak K. Shandiz 2025-09-23 15:24:37 +01:00
parent 914531e6f1
commit 5fddcef0a8
No known key found for this signature in database
GPG key ID: 9472CAEFF56C742E
3 changed files with 161 additions and 225 deletions

View file

@ -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())
}

View file

@ -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
}

View file

@ -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"})
}