cli/internal/config/auth_config_test.go

367 lines
12 KiB
Go

package config
import (
"errors"
"testing"
ghConfig "github.com/cli/go-gh/v2/pkg/config"
"github.com/stretchr/testify/require"
"github.com/zalando/go-keyring"
)
func newTestAuthConfig(t *testing.T) *AuthConfig {
authCfg := AuthConfig{
cfg: ghConfig.ReadFromString(""),
}
// The real implementation of config.Read uses a sync.Once
// to read config files and initialise package level variables
// that are used from then on.
//
// This means that tests can't be isolated from each other, so
// we swap out the function here to return a new config each time.
ghConfig.Read = func(_ *ghConfig.Config) (*ghConfig.Config, error) {
return authCfg.cfg, nil
}
// The config.Write method isn't defined in the same way as Read to allow
// the function to be swapped out and it does try to write to disk.
//
// We should consider whether it makes sense to change that but in the meantime
// we can use GH_CONFIG_DIR env var to ensure the tests remain isolated.
StubWriteConfig(t)
return &authCfg
}
func TestTokenFromKeyring(t *testing.T) {
// Given a keyring that contains a token for a host
keyring.MockInit()
require.NoError(t, keyring.Set(keyringServiceName("github.com"), "", "test-token"))
// When we get the token from the auth config
authCfg := newTestAuthConfig(t)
token, err := authCfg.TokenFromKeyring("github.com")
// Then it returns successfully with the correct token
require.NoError(t, err)
require.Equal(t, "test-token", token)
}
func TestTokenStoredInConfig(t *testing.T) {
// When the user has logged in insecurely
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "", false)
require.NoError(t, err)
// When we get the token
token, source := authCfg.Token("github.com")
// Then the token is successfully fetched
// and the source is set to oauth_token but this isn't great:
// https://github.com/cli/go-gh/issues/94
require.Equal(t, "test-token", token)
require.Equal(t, "oauth_token", source)
}
func TestTokenStoredInEnv(t *testing.T) {
// When the user is authenticated via env var
authCfg := newTestAuthConfig(t)
t.Setenv("GH_TOKEN", "test-token")
// When we get the token
token, source := authCfg.Token("github.com")
// Then the token is successfully fetched
// and the source is set to the name of the env var
require.Equal(t, "test-token", token)
require.Equal(t, "GH_TOKEN", source)
}
func TestTokenStoredInKeyring(t *testing.T) {
// When the user has logged in securely
keyring.MockInit()
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "", true)
require.NoError(t, err)
// When we get the token
token, source := authCfg.Token("github.com")
// Then the token is successfully fetched
// and the source is set to keyring
require.Equal(t, "test-token", token)
require.Equal(t, "keyring", source)
}
func TestTokenFromKeyringNonExistent(t *testing.T) {
// Given a keyring that doesn't contain any tokens
keyring.MockInit()
// When we try to get a token from the auth config
authCfg := newTestAuthConfig(t)
_, err := authCfg.TokenFromKeyring("github.com")
// Then it returns failure bubbling the ErrNotFound
require.ErrorIs(t, err, keyring.ErrNotFound)
}
func TestHasEnvTokenWithoutAnyEnvToken(t *testing.T) {
// Given we have no env set
authCfg := newTestAuthConfig(t)
// When we check if it has an env token
hasEnvToken := authCfg.HasEnvToken()
// Then it returns false
require.False(t, hasEnvToken, "expected not to have env token")
}
func TestHasEnvTokenWithEnvToken(t *testing.T) {
// Given we have an env token set
// Note that any valid env var for tokens will do, not just GH_ENTERPRISE_TOKEN
authCfg := newTestAuthConfig(t)
t.Setenv("GH_ENTERPRISE_TOKEN", "test-token")
// When we check if it has an env token
hasEnvToken := authCfg.HasEnvToken()
// Then it returns true
require.True(t, hasEnvToken, "expected to have env token")
}
func TestHasEnvTokenWithNoEnvTokenButAConfigVar(t *testing.T) {
t.Skip("this test is explicitly breaking some implementation assumptions")
// Given a token in the config
authCfg := newTestAuthConfig(t)
// Using example.com here will cause the token to be returned from the config
_, err := authCfg.Login("example.com", "test-user", "test-token", "", false)
require.NoError(t, err)
// When we check if it has an env token
hasEnvToken := authCfg.HasEnvToken()
// Then it SHOULD return false
require.False(t, hasEnvToken, "expected not to have env token")
}
func TestUserNotLoggedIn(t *testing.T) {
// Given we have not logged in
authCfg := newTestAuthConfig(t)
// When we get the user
_, err := authCfg.User("github.com")
// Then it returns failure, bubbling the KeyNotFoundError
var keyNotFoundError *ghConfig.KeyNotFoundError
require.ErrorAs(t, err, &keyNotFoundError)
}
func TestHostsIncludesEnvVar(t *testing.T) {
// Given the GH_HOST env var is set
authCfg := newTestAuthConfig(t)
t.Setenv("GH_HOST", "ghe.io")
// When we get the hosts
hosts := authCfg.Hosts()
// Then the host in the env var is included
require.Contains(t, hosts, "ghe.io")
}
func TestDefaultHostFromEnvVar(t *testing.T) {
// Given the GH_HOST env var is set
authCfg := newTestAuthConfig(t)
t.Setenv("GH_HOST", "ghe.io")
// When we get the DefaultHost
defaultHost, source := authCfg.DefaultHost()
// Then the returned host and source are using the env var
require.Equal(t, "ghe.io", defaultHost)
require.Equal(t, "GH_HOST", source)
}
func TestDefaultHostNotLoggedIn(t *testing.T) {
// Given we are not logged in
authCfg := newTestAuthConfig(t)
// When we get the DefaultHost
defaultHost, source := authCfg.DefaultHost()
// Then the returned host is always github.com
require.Equal(t, "github.com", defaultHost)
require.Equal(t, "default", source)
}
func TestDefaultHostLoggedInToOnlyOneHost(t *testing.T) {
// Given we are logged into one host (not github.com to differentiate from the fallback)
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("ghe.io", "test-user", "test-token", "", false)
require.NoError(t, err)
// When we get the DefaultHost
defaultHost, source := authCfg.DefaultHost()
// Then the returned host is that logged in host and the source is the hosts config
require.Equal(t, "ghe.io", defaultHost)
require.Equal(t, hostsKey, source)
}
func TestLoginSecureStorageUsesKeyring(t *testing.T) {
// Given a usable keyring
keyring.MockInit()
authCfg := newTestAuthConfig(t)
// When we login with secure storage
insecureStorageUsed, err := authCfg.Login("github.com", "test-user", "test-token", "", true)
// Then it returns success, notes that insecure storage was not used, and stores the token in the keyring
require.NoError(t, err)
require.False(t, insecureStorageUsed, "expected to use secure storage")
token, err := keyring.Get(keyringServiceName("github.com"), "")
require.NoError(t, err)
require.Equal(t, "test-token", token)
}
func TestLoginSecureStorageRemovesOldInsecureConfigToken(t *testing.T) {
// Given a usable keyring and an oauth token in the config
keyring.MockInit()
authCfg := newTestAuthConfig(t)
authCfg.cfg.Set([]string{hostsKey, "github.com", oauthTokenKey}, "old-token")
// When we login with secure storage
_, err := authCfg.Login("github.com", "test-user", "test-token", "", true)
// Then it returns success, having also removed the old token from the config
require.NoError(t, err)
requireNoKey(t, authCfg.cfg, []string{hostsKey, "github.com", oauthTokenKey})
}
func TestLoginSecureStorageWithErrorFallsbackAndReports(t *testing.T) {
// Given a keyring that errors
keyring.MockInitWithError(errors.New("test-explosion"))
authCfg := newTestAuthConfig(t)
// When we login with secure storage
insecureStorageUsed, err := authCfg.Login("github.com", "test-user", "test-token", "", true)
// Then it returns success, reports that insecure storage was used, and stores the token in the config
require.NoError(t, err)
require.True(t, insecureStorageUsed, "expected to use insecure storage")
requireKeyWithValue(t, authCfg.cfg, []string{hostsKey, "github.com", oauthTokenKey}, "test-token")
}
func TestLoginInsecureStorage(t *testing.T) {
// Given we are not logged in
authCfg := newTestAuthConfig(t)
// When we login with insecure storage
insecureStorageUsed, err := authCfg.Login("github.com", "test-user", "test-token", "", false)
// Then it returns success, notes that insecure storage was used, and stores the token in the config
require.NoError(t, err)
require.True(t, insecureStorageUsed, "expected to use insecure storage")
requireKeyWithValue(t, authCfg.cfg, []string{hostsKey, "github.com", oauthTokenKey}, "test-token")
}
func TestLoginSetsUserForProvidedHost(t *testing.T) {
// Given we are not logged in
authCfg := newTestAuthConfig(t)
// When we login
_, err := authCfg.Login("github.com", "test-user", "test-token", "ssh", false)
// Then it returns success and the user is set
require.NoError(t, err)
user, err := authCfg.User("github.com")
require.NoError(t, err)
require.Equal(t, "test-user", user)
}
func TestLoginSetsGitProtocolForProvidedHost(t *testing.T) {
// Given we are loggedin
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "ssh", false)
require.NoError(t, err)
// When we get the git protocol
protocol, err := authCfg.cfg.Get([]string{hostsKey, "github.com", gitProtocolKey})
require.NoError(t, err)
// Then it returns the git protocol we provided on login
require.Equal(t, "ssh", protocol)
}
func TestLoginAddsHostIfNotAlreadyAdded(t *testing.T) {
// Given we are logged in
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "ssh", false)
require.NoError(t, err)
// When we get the hosts
hosts := authCfg.Hosts()
// Then it includes our logged in host
require.Contains(t, hosts, "github.com")
}
func TestLogoutRemovesHostAndKeyringToken(t *testing.T) {
// Given we are logged into a host
keyring.MockInit()
authCfg := newTestAuthConfig(t)
_, err := authCfg.Login("github.com", "test-user", "test-token", "ssh", true)
require.NoError(t, err)
// When we logout
err = authCfg.Logout("github.com")
// Then we return success, and the host and token are removed from the config and keyring
require.NoError(t, err)
requireNoKey(t, authCfg.cfg, []string{hostsKey, "github.com"})
_, err = keyring.Get(keyringServiceName("github.com"), "")
require.ErrorIs(t, err, keyring.ErrNotFound)
}
// Note that I'm not sure this test enforces particularly desirable behaviour
// since it leads users to believe a token has been removed when really
// that might have failed for some reason.
//
// The original intention here is that if the logout fails, the user can't
// really do anything to recover. On the other hand, a user might
// want to rectify this manually, for example if there were on a shared machine.
func TestLogoutIgnoresErrorsFromConfigAndKeyring(t *testing.T) {
// Given we have keyring that errors, and a config that
// doesn't even have a hosts key (which would cause Remove to fail)
keyring.MockInitWithError(errors.New("test-explosion"))
authCfg := newTestAuthConfig(t)
// When we logout
err := authCfg.Logout("github.com")
// Then it returns success anyway, suppressing the errors
require.NoError(t, err)
}
func requireKeyWithValue(t *testing.T, cfg *ghConfig.Config, keys []string, value string) {
t.Helper()
actual, err := cfg.Get(keys)
require.NoError(t, err)
require.Equal(t, value, actual)
}
func requireNoKey(t *testing.T, cfg *ghConfig.Config, keys []string) {
t.Helper()
_, err := cfg.Get(keys)
var keyNotFoundError *ghConfig.KeyNotFoundError
require.ErrorAs(t, err, &keyNotFoundError)
}