440 lines
13 KiB
Go
440 lines
13 KiB
Go
package authswitch
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/cli/cli/v2/internal/config"
|
|
"github.com/cli/cli/v2/internal/gh"
|
|
"github.com/cli/cli/v2/internal/keyring"
|
|
"github.com/cli/cli/v2/internal/prompter"
|
|
"github.com/cli/cli/v2/pkg/cmdutil"
|
|
"github.com/cli/cli/v2/pkg/iostreams"
|
|
"github.com/google/shlex"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewCmdSwitch(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expectedOpts SwitchOptions
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "no flags",
|
|
input: "",
|
|
expectedOpts: SwitchOptions{},
|
|
},
|
|
{
|
|
name: "hostname flag",
|
|
input: "--hostname github.com",
|
|
expectedOpts: SwitchOptions{
|
|
Hostname: "github.com",
|
|
},
|
|
},
|
|
{
|
|
name: "user flag",
|
|
input: "--user monalisa",
|
|
expectedOpts: SwitchOptions{
|
|
Username: "monalisa",
|
|
},
|
|
},
|
|
{
|
|
name: "positional args is an error",
|
|
input: "some-positional-arg",
|
|
expectedErrMsg: "accepts 0 arg(s), received 1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
f := &cmdutil.Factory{}
|
|
argv, err := shlex.Split(tt.input)
|
|
require.NoError(t, err)
|
|
|
|
var gotOpts *SwitchOptions
|
|
cmd := NewCmdSwitch(f, func(opts *SwitchOptions) error {
|
|
gotOpts = opts
|
|
return nil
|
|
})
|
|
// Override the help flag as happens in production to allow -h flag
|
|
// to be used for hostname.
|
|
cmd.Flags().BoolP("help", "x", false, "")
|
|
|
|
cmd.SetArgs(argv)
|
|
cmd.SetIn(&bytes.Buffer{})
|
|
cmd.SetOut(&bytes.Buffer{})
|
|
cmd.SetErr(&bytes.Buffer{})
|
|
|
|
_, err = cmd.ExecuteC()
|
|
if tt.expectedErrMsg != "" {
|
|
require.ErrorContains(t, err, tt.expectedErrMsg)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.Equal(t, &tt.expectedOpts, gotOpts)
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
func TestSwitchRun(t *testing.T) {
|
|
type user struct {
|
|
name string
|
|
token string
|
|
}
|
|
|
|
type hostUsers struct {
|
|
host string
|
|
users []user
|
|
}
|
|
|
|
type successfulExpectation struct {
|
|
switchedHost string
|
|
activeUser string
|
|
activeToken string
|
|
hostsCfg string
|
|
stderr string
|
|
}
|
|
|
|
type failedExpectation struct {
|
|
err error
|
|
stderr string
|
|
}
|
|
|
|
userWithMissingToken := "user-that-is-broken-by-the-test"
|
|
|
|
tests := []struct {
|
|
name string
|
|
opts SwitchOptions
|
|
cfgHosts []hostUsers
|
|
env map[string]string
|
|
|
|
expectedSuccess successfulExpectation
|
|
expectedFailure failedExpectation
|
|
|
|
prompterStubs func(*prompter.PrompterMock)
|
|
}{
|
|
{
|
|
name: "given one host with two users, switches to the other user",
|
|
opts: SwitchOptions{},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedSuccess: successfulExpectation{
|
|
switchedHost: "github.com",
|
|
activeUser: "inactive-user",
|
|
activeToken: "inactive-user-token",
|
|
hostsCfg: "github.com:\n git_protocol: ssh\n users:\n inactive-user:\n active-user:\n user: inactive-user\n",
|
|
stderr: "✓ Switched active account for github.com to inactive-user",
|
|
},
|
|
},
|
|
{
|
|
name: "given one host, with three users, switches to the specified user",
|
|
opts: SwitchOptions{
|
|
Username: "inactive-user-2",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user-1", "inactive-user-1-token"},
|
|
{"inactive-user-2", "inactive-user-2-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedSuccess: successfulExpectation{
|
|
switchedHost: "github.com",
|
|
activeUser: "inactive-user-2",
|
|
activeToken: "inactive-user-2-token",
|
|
hostsCfg: "github.com:\n git_protocol: ssh\n users:\n inactive-user-1:\n inactive-user-2:\n active-user:\n user: inactive-user-2\n",
|
|
stderr: "✓ Switched active account for github.com to inactive-user-2",
|
|
},
|
|
},
|
|
{
|
|
name: "given multiple hosts, with multiple users, switches to the specific user on the host",
|
|
opts: SwitchOptions{
|
|
Hostname: "ghe.io",
|
|
Username: "inactive-user",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
{"ghe.io", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedSuccess: successfulExpectation{
|
|
switchedHost: "ghe.io",
|
|
activeUser: "inactive-user",
|
|
activeToken: "inactive-user-token",
|
|
hostsCfg: "github.com:\n git_protocol: ssh\n users:\n inactive-user:\n active-user:\n user: active-user\nghe.io:\n git_protocol: ssh\n users:\n inactive-user:\n active-user:\n user: inactive-user\n",
|
|
stderr: "✓ Switched active account for ghe.io to inactive-user",
|
|
},
|
|
},
|
|
{
|
|
name: "given we're not logged into any hosts, provide an informative error",
|
|
opts: SwitchOptions{},
|
|
cfgHosts: []hostUsers{},
|
|
expectedFailure: failedExpectation{
|
|
err: errors.New("not logged in to any hosts"),
|
|
},
|
|
},
|
|
{
|
|
name: "given we can't disambiguate users across hosts",
|
|
opts: SwitchOptions{
|
|
Username: "inactive-user",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
{"ghe.io", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedFailure: failedExpectation{
|
|
err: errors.New("unable to determine which account to switch to, please specify `--hostname` and `--user`"),
|
|
},
|
|
},
|
|
{
|
|
name: "given we can't disambiguate user on a single host",
|
|
opts: SwitchOptions{
|
|
Hostname: "github.com",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user-1", "inactive-user-1-token"},
|
|
{"inactive-user-2", "inactive-user-2-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedFailure: failedExpectation{
|
|
err: errors.New("unable to determine which account to switch to, please specify `--hostname` and `--user`"),
|
|
},
|
|
},
|
|
{
|
|
name: "given the auth token isn't writeable (e.g. a token env var is set)",
|
|
opts: SwitchOptions{},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
env: map[string]string{"GH_TOKEN": "unimportant-test-value"},
|
|
expectedFailure: failedExpectation{
|
|
err: cmdutil.SilentError,
|
|
stderr: "The value of the GH_TOKEN environment variable is being used for authentication.",
|
|
},
|
|
},
|
|
{
|
|
name: "specified hostname doesn't exist",
|
|
opts: SwitchOptions{
|
|
Hostname: "ghe.io",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedFailure: failedExpectation{
|
|
err: errors.New("not logged in to ghe.io"),
|
|
},
|
|
},
|
|
{
|
|
name: "specified user doesn't exist on host",
|
|
opts: SwitchOptions{
|
|
Hostname: "github.com",
|
|
Username: "non-existent-user",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedFailure: failedExpectation{
|
|
err: errors.New("not logged in to github.com account non-existent-user"),
|
|
},
|
|
},
|
|
{
|
|
name: "specified user doesn't exist on any host",
|
|
opts: SwitchOptions{
|
|
Username: "non-existent-user",
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
{"ghe.io", []user{
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedFailure: failedExpectation{
|
|
err: errors.New("no accounts matched that criteria"),
|
|
},
|
|
},
|
|
{
|
|
name: "when options need to be disambiguated, the user is prompted with matrix of options including active users (if possible)",
|
|
opts: SwitchOptions{},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
{"ghe.io", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
prompterStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
|
|
require.Equal(t, "What account do you want to switch to?", prompt)
|
|
require.Equal(t, []string{
|
|
"inactive-user (github.com)",
|
|
"active-user (github.com) - active",
|
|
"inactive-user (ghe.io)",
|
|
"active-user (ghe.io) - active",
|
|
}, opts)
|
|
|
|
return prompter.IndexFor(opts, "inactive-user (ghe.io)")
|
|
}
|
|
},
|
|
expectedSuccess: successfulExpectation{
|
|
switchedHost: "ghe.io",
|
|
activeUser: "inactive-user",
|
|
activeToken: "inactive-user-token",
|
|
hostsCfg: "github.com:\n git_protocol: ssh\n users:\n inactive-user:\n active-user:\n user: active-user\nghe.io:\n git_protocol: ssh\n users:\n inactive-user:\n active-user:\n user: inactive-user\n",
|
|
stderr: "✓ Switched active account for ghe.io to inactive-user",
|
|
},
|
|
},
|
|
{
|
|
name: "options need to be disambiguated given two hosts, one with two users",
|
|
opts: SwitchOptions{},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{"inactive-user", "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
{"ghe.io", []user{
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
prompterStubs: func(pm *prompter.PrompterMock) {
|
|
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
|
|
require.Equal(t, "What account do you want to switch to?", prompt)
|
|
require.Equal(t, []string{
|
|
"inactive-user (github.com)",
|
|
"active-user (github.com) - active",
|
|
"active-user (ghe.io) - active",
|
|
}, opts)
|
|
|
|
return prompter.IndexFor(opts, "inactive-user (github.com)")
|
|
}
|
|
},
|
|
expectedSuccess: successfulExpectation{
|
|
switchedHost: "github.com",
|
|
activeUser: "inactive-user",
|
|
activeToken: "inactive-user-token",
|
|
hostsCfg: "github.com:\n git_protocol: ssh\n users:\n inactive-user:\n active-user:\n user: inactive-user\nghe.io:\n git_protocol: ssh\n users:\n active-user:\n user: active-user\n",
|
|
stderr: "✓ Switched active account for github.com to inactive-user",
|
|
},
|
|
},
|
|
{
|
|
name: "when switching fails due to something other than user error, an informative message is printed to explain their new state",
|
|
opts: SwitchOptions{
|
|
Username: userWithMissingToken,
|
|
},
|
|
cfgHosts: []hostUsers{
|
|
{"github.com", []user{
|
|
{userWithMissingToken, "inactive-user-token"},
|
|
{"active-user", "active-user-token"},
|
|
}},
|
|
},
|
|
expectedFailure: failedExpectation{
|
|
err: fmt.Errorf("no token found for %s", userWithMissingToken),
|
|
stderr: fmt.Sprintf("X Failed to switch account for github.com to %s", userWithMissingToken),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cfg, readConfigs := config.NewIsolatedTestConfig(t)
|
|
|
|
for k, v := range tt.env {
|
|
t.Setenv(k, v)
|
|
}
|
|
|
|
isInteractive := tt.prompterStubs != nil
|
|
if isInteractive {
|
|
pm := &prompter.PrompterMock{}
|
|
tt.prompterStubs(pm)
|
|
tt.opts.Prompter = pm
|
|
defer func() {
|
|
require.Len(t, pm.SelectCalls(), 1)
|
|
}()
|
|
}
|
|
|
|
for _, hostUsers := range tt.cfgHosts {
|
|
for _, user := range hostUsers.users {
|
|
_, err := cfg.Authentication().Login(
|
|
hostUsers.host,
|
|
user.name,
|
|
user.token, "ssh", true,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
if user.name == userWithMissingToken {
|
|
require.NoError(t, keyring.Delete(fmt.Sprintf("gh:%s", hostUsers.host), userWithMissingToken))
|
|
}
|
|
}
|
|
}
|
|
|
|
tt.opts.Config = func() (gh.Config, error) {
|
|
return cfg, nil
|
|
}
|
|
|
|
ios, _, _, stderr := iostreams.Test()
|
|
ios.SetStdinTTY(isInteractive)
|
|
ios.SetStdoutTTY(isInteractive)
|
|
tt.opts.IO = ios
|
|
|
|
err := switchRun(&tt.opts)
|
|
if tt.expectedFailure.err != nil {
|
|
require.Equal(t, tt.expectedFailure.err, err)
|
|
require.Contains(t, stderr.String(), tt.expectedFailure.stderr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
|
|
activeUser, err := cfg.Authentication().ActiveUser(tt.expectedSuccess.switchedHost)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expectedSuccess.activeUser, activeUser)
|
|
|
|
activeToken, _ := cfg.Authentication().TokenFromKeyring(tt.expectedSuccess.switchedHost)
|
|
require.Equal(t, tt.expectedSuccess.activeToken, activeToken)
|
|
|
|
hostsBuf := bytes.Buffer{}
|
|
readConfigs(io.Discard, &hostsBuf)
|
|
|
|
require.Equal(t, tt.expectedSuccess.hostsCfg, hostsBuf.String())
|
|
|
|
require.Contains(t, stderr.String(), tt.expectedSuccess.stderr)
|
|
})
|
|
}
|
|
}
|