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