DockerCLI/cli/command/registry/login_test.go

229 lines
6.0 KiB
Go

package registry
import (
"bytes"
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/creack/pty"
"github.com/docker/cli/cli/command"
configtypes "github.com/docker/cli/cli/config/types"
"github.com/docker/cli/cli/streams"
"github.com/docker/cli/internal/test"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/system"
"github.com/docker/docker/client"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/fs"
)
const (
unknownUser = "userunknownError"
errUnknownUser = "UNKNOWN_ERR"
expiredPassword = "I_M_EXPIRED"
useToken = "I_M_TOKEN"
)
type fakeClient struct {
client.Client
}
func (c *fakeClient) Info(context.Context) (system.Info, error) {
return system.Info{}, nil
}
func (c *fakeClient) RegistryLogin(_ context.Context, auth registrytypes.AuthConfig) (registrytypes.AuthenticateOKBody, error) {
if auth.Password == expiredPassword {
return registrytypes.AuthenticateOKBody{}, errors.New("Invalid Username or Password")
}
if auth.Password == useToken {
return registrytypes.AuthenticateOKBody{
IdentityToken: auth.Password,
}, nil
}
if auth.Username == unknownUser {
return registrytypes.AuthenticateOKBody{}, errors.New(errUnknownUser)
}
return registrytypes.AuthenticateOKBody{}, nil
}
func TestLoginWithCredStoreCreds(t *testing.T) {
testCases := []struct {
inputAuthConfig registrytypes.AuthConfig
expectedMsg string
expectedErr string
}{
{
inputAuthConfig: registrytypes.AuthConfig{},
expectedMsg: "Authenticating with existing credentials...\n",
},
{
inputAuthConfig: registrytypes.AuthConfig{
Username: unknownUser,
},
expectedMsg: "Authenticating with existing credentials...\n",
expectedErr: fmt.Sprintf("Login did not succeed, error: %s\n", errUnknownUser),
},
}
ctx := context.Background()
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
errBuf := new(bytes.Buffer)
cli.SetErr(streams.NewOut(errBuf))
loginWithCredStoreCreds(ctx, cli, &tc.inputAuthConfig)
outputString := cli.OutBuffer().String()
assert.Check(t, is.Equal(tc.expectedMsg, outputString))
errorString := errBuf.String()
assert.Check(t, is.Equal(tc.expectedErr, errorString))
}
}
func TestRunLogin(t *testing.T) {
const (
storedServerAddress = "reg1"
validUsername = "u1"
validPassword = "p1"
validPassword2 = "p2"
)
validAuthConfig := configtypes.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: validPassword,
}
expiredAuthConfig := configtypes.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: expiredPassword,
}
validIdentityToken := configtypes.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
IdentityToken: useToken,
}
testCases := []struct {
doc string
inputLoginOption loginOptions
inputStoredCred *configtypes.AuthConfig
expectedErr string
expectedSavedCred configtypes.AuthConfig
}{
{
doc: "valid auth from store",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
},
inputStoredCred: &validAuthConfig,
expectedSavedCred: validAuthConfig,
},
{
doc: "expired auth",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
},
inputStoredCred: &expiredAuthConfig,
expectedErr: "Error: Cannot perform an interactive login from a non TTY device",
},
{
doc: "valid username and password",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
user: validUsername,
password: validPassword2,
},
inputStoredCred: &validAuthConfig,
expectedSavedCred: configtypes.AuthConfig{
ServerAddress: storedServerAddress,
Username: validUsername,
Password: validPassword2,
},
},
{
doc: "unknown user",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
user: unknownUser,
password: validPassword,
},
inputStoredCred: &validAuthConfig,
expectedErr: errUnknownUser,
},
{
doc: "valid token",
inputLoginOption: loginOptions{
serverAddress: storedServerAddress,
user: validUsername,
password: useToken,
},
inputStoredCred: &validIdentityToken,
expectedSavedCred: validIdentityToken,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.doc, func(t *testing.T) {
tmpFile := fs.NewFile(t, "test-run-login")
defer tmpFile.Remove()
cli := test.NewFakeCli(&fakeClient{})
configfile := cli.ConfigFile()
configfile.Filename = tmpFile.Path()
if tc.inputStoredCred != nil {
cred := *tc.inputStoredCred
assert.NilError(t, configfile.GetCredentialsStore(cred.ServerAddress).Store(cred))
}
loginErr := runLogin(context.Background(), cli, tc.inputLoginOption)
if tc.expectedErr != "" {
assert.Error(t, loginErr, tc.expectedErr)
return
}
assert.NilError(t, loginErr)
savedCred, credStoreErr := configfile.GetCredentialsStore(tc.inputStoredCred.ServerAddress).Get(tc.inputStoredCred.ServerAddress)
assert.Check(t, credStoreErr)
assert.DeepEqual(t, tc.expectedSavedCred, savedCred)
})
}
}
func TestLoginTermination(t *testing.T) {
p, tty, err := pty.Open()
assert.NilError(t, err)
t.Cleanup(func() {
_ = tty.Close()
_ = p.Close()
})
cli := test.NewFakeCli(&fakeClient{}, func(fc *test.FakeCli) {
fc.SetOut(streams.NewOut(tty))
fc.SetIn(streams.NewIn(tty))
})
tmpFile := fs.NewFile(t, "test-login-termination")
defer tmpFile.Remove()
configFile := cli.ConfigFile()
configFile.Filename = tmpFile.Path()
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
runErr := make(chan error)
go func() {
runErr <- runLogin(ctx, cli, loginOptions{})
}()
// Let the prompt get canceled by the context
cancel()
select {
case <-time.After(1 * time.Second):
t.Fatal("timed out after 1 second. `runLogin` did not return")
case err := <-runErr:
assert.ErrorIs(t, err, command.ErrPromptTerminated)
}
}