Explorar o código

Merge pull request #32248 from icapurro/cli-image-tests

Unit tests for cli/command/image package
Vincent Demeester %!s(int64=8) %!d(string=hai) anos
pai
achega
663f0ba1b2
Modificáronse 58 ficheiros con 1445 adicións e 123 borrados
  1. 7 0
      cli/command/cli.go
  2. 5 5
      cli/command/container/exec_test.go
  3. 116 0
      cli/command/image/client_test.go
  4. 2 2
      cli/command/image/history.go
  5. 108 0
      cli/command/image/history_test.go
  6. 2 2
      cli/command/image/import.go
  7. 100 0
      cli/command/image/import_test.go
  8. 2 2
      cli/command/image/inspect.go
  9. 92 0
      cli/command/image/inspect_test.go
  10. 3 3
      cli/command/image/list.go
  11. 102 0
      cli/command/image/list_test.go
  12. 2 2
      cli/command/image/load.go
  13. 106 0
      cli/command/image/load_test.go
  14. 3 3
      cli/command/image/prune.go
  15. 100 0
      cli/command/image/prune_test.go
  16. 2 2
      cli/command/image/pull.go
  17. 85 0
      cli/command/image/pull_test.go
  18. 2 2
      cli/command/image/push.go
  19. 84 0
      cli/command/image/push_test.go
  20. 3 3
      cli/command/image/remove.go
  21. 103 0
      cli/command/image/remove_test.go
  22. 2 2
      cli/command/image/save.go
  23. 98 0
      cli/command/image/save_test.go
  24. 2 2
      cli/command/image/tag.go
  25. 43 0
      cli/command/image/tag_test.go
  26. 1 0
      cli/command/image/testdata/history-command-success.quiet-no-trunc.golden
  27. 1 0
      cli/command/image/testdata/history-command-success.quiet.golden
  28. 2 0
      cli/command/image/testdata/history-command-success.simple.golden
  29. 1 0
      cli/command/image/testdata/import-command-success.input.txt
  30. 1 0
      cli/command/image/testdata/inspect-command-success.format.golden
  31. 50 0
      cli/command/image/testdata/inspect-command-success.simple-many.golden
  32. 26 0
      cli/command/image/testdata/inspect-command-success.simple.golden
  33. 1 0
      cli/command/image/testdata/list-command-success.filters.golden
  34. 0 0
      cli/command/image/testdata/list-command-success.format.golden
  35. 1 0
      cli/command/image/testdata/list-command-success.match-name.golden
  36. 0 0
      cli/command/image/testdata/list-command-success.quiet-format.golden
  37. 1 0
      cli/command/image/testdata/list-command-success.simple.golden
  38. 1 0
      cli/command/image/testdata/load-command-success.input-file.golden
  39. 1 0
      cli/command/image/testdata/load-command-success.input.txt
  40. 1 0
      cli/command/image/testdata/load-command-success.json.golden
  41. 1 0
      cli/command/image/testdata/load-command-success.simple.golden
  42. 2 0
      cli/command/image/testdata/prune-command-success.all.golden
  43. 4 0
      cli/command/image/testdata/prune-command-success.force-deleted.golden
  44. 4 0
      cli/command/image/testdata/prune-command-success.force-untagged.golden
  45. 1 0
      cli/command/image/testdata/pull-command-success.simple-no-tag.golden
  46. 0 0
      cli/command/image/testdata/pull-command-success.simple.golden
  47. 4 0
      cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden
  48. 2 0
      cli/command/image/testdata/remove-command-success.Image Deleted.golden
  49. 2 0
      cli/command/image/testdata/remove-command-success.Image Untagged.golden
  50. 7 7
      cli/command/image/trust.go
  51. 5 35
      cli/command/in.go
  52. 4 34
      cli/command/out.go
  53. 7 7
      cli/command/registry.go
  54. 44 0
      cli/command/stream.go
  55. 2 1
      cli/command/swarm/unlock_test.go
  56. 3 2
      cli/command/volume/prune_test.go
  57. 17 7
      cli/internal/test/cli.go
  58. 74 0
      cli/internal/test/store.go

+ 7 - 0
cli/command/cli.go

@@ -39,7 +39,9 @@ type Cli interface {
 	Out() *OutStream
 	Err() io.Writer
 	In() *InStream
+	SetIn(in *InStream)
 	ConfigFile() *configfile.ConfigFile
+	CredentialsStore(serverAddress string) credentials.Store
 }
 
 // DockerCli is an instance the docker command line client.
@@ -75,6 +77,11 @@ func (cli *DockerCli) Err() io.Writer {
 	return cli.err
 }
 
+// SetIn sets the reader used for stdin
+func (cli *DockerCli) SetIn(in *InStream) {
+	cli.in = in
+}
+
 // In returns the reader used for stdin
 func (cli *DockerCli) In() *InStream {
 	return cli.in

+ 5 - 5
cli/command/container/exec_test.go

@@ -13,21 +13,21 @@ type arguments struct {
 
 func TestParseExec(t *testing.T) {
 	valids := map[*arguments]*types.ExecConfig{
-		&arguments{
+		{
 			execCmd: []string{"command"},
 		}: {
 			Cmd:          []string{"command"},
 			AttachStdout: true,
 			AttachStderr: true,
 		},
-		&arguments{
+		{
 			execCmd: []string{"command1", "command2"},
 		}: {
 			Cmd:          []string{"command1", "command2"},
 			AttachStdout: true,
 			AttachStderr: true,
 		},
-		&arguments{
+		{
 			options: execOptions{
 				interactive: true,
 				tty:         true,
@@ -42,7 +42,7 @@ func TestParseExec(t *testing.T) {
 			Tty:          true,
 			Cmd:          []string{"command"},
 		},
-		&arguments{
+		{
 			options: execOptions{
 				detach: true,
 			},
@@ -54,7 +54,7 @@ func TestParseExec(t *testing.T) {
 			Detach:       true,
 			Cmd:          []string{"command"},
 		},
-		&arguments{
+		{
 			options: execOptions{
 				tty:         true,
 				interactive: true,

+ 116 - 0
cli/command/image/client_test.go

@@ -0,0 +1,116 @@
+package image
+
+import (
+	"io"
+	"io/ioutil"
+	"strings"
+	"time"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/api/types/image"
+	"github.com/docker/docker/client"
+	"golang.org/x/net/context"
+)
+
+type fakeClient struct {
+	client.Client
+	imageTagFunc     func(string, string) error
+	imageSaveFunc    func(images []string) (io.ReadCloser, error)
+	imageRemoveFunc  func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
+	imagePushFunc    func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
+	infoFunc         func() (types.Info, error)
+	imagePullFunc    func(ref string, options types.ImagePullOptions) (io.ReadCloser, error)
+	imagesPruneFunc  func(pruneFilter filters.Args) (types.ImagesPruneReport, error)
+	imageLoadFunc    func(input io.Reader, quiet bool) (types.ImageLoadResponse, error)
+	imageListFunc    func(options types.ImageListOptions) ([]types.ImageSummary, error)
+	imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
+	imageImportFunc  func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
+	imageHistoryFunc func(image string) ([]image.HistoryResponseItem, error)
+}
+
+func (cli *fakeClient) ImageTag(_ context.Context, image, ref string) error {
+	if cli.imageTagFunc != nil {
+		return cli.imageTagFunc(image, ref)
+	}
+	return nil
+}
+
+func (cli *fakeClient) ImageSave(_ context.Context, images []string) (io.ReadCloser, error) {
+	if cli.imageSaveFunc != nil {
+		return cli.imageSaveFunc(images)
+	}
+	return ioutil.NopCloser(strings.NewReader("")), nil
+}
+
+func (cli *fakeClient) ImageRemove(_ context.Context, image string,
+	options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
+	if cli.imageRemoveFunc != nil {
+		return cli.imageRemoveFunc(image, options)
+	}
+	return []types.ImageDeleteResponseItem{}, nil
+}
+
+func (cli *fakeClient) ImagePush(_ context.Context, ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
+	if cli.imagePushFunc != nil {
+		return cli.imagePushFunc(ref, options)
+	}
+	return ioutil.NopCloser(strings.NewReader("")), nil
+}
+
+func (cli *fakeClient) Info(_ context.Context) (types.Info, error) {
+	if cli.infoFunc != nil {
+		return cli.infoFunc()
+	}
+	return types.Info{}, nil
+}
+
+func (cli *fakeClient) ImagePull(_ context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {
+	if cli.imagePullFunc != nil {
+		cli.imagePullFunc(ref, options)
+	}
+	return ioutil.NopCloser(strings.NewReader("")), nil
+}
+
+func (cli *fakeClient) ImagesPrune(_ context.Context, pruneFilter filters.Args) (types.ImagesPruneReport, error) {
+	if cli.imagesPruneFunc != nil {
+		return cli.imagesPruneFunc(pruneFilter)
+	}
+	return types.ImagesPruneReport{}, nil
+}
+
+func (cli *fakeClient) ImageLoad(_ context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
+	if cli.imageLoadFunc != nil {
+		return cli.imageLoadFunc(input, quiet)
+	}
+	return types.ImageLoadResponse{}, nil
+}
+
+func (cli *fakeClient) ImageList(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) {
+	if cli.imageListFunc != nil {
+		return cli.imageListFunc(options)
+	}
+	return []types.ImageSummary{{}}, nil
+}
+
+func (cli *fakeClient) ImageInspectWithRaw(_ context.Context, image string) (types.ImageInspect, []byte, error) {
+	if cli.imageInspectFunc != nil {
+		return cli.imageInspectFunc(image)
+	}
+	return types.ImageInspect{}, nil, nil
+}
+
+func (cli *fakeClient) ImageImport(_ context.Context, source types.ImageImportSource, ref string,
+	options types.ImageImportOptions) (io.ReadCloser, error) {
+	if cli.imageImportFunc != nil {
+		return cli.imageImportFunc(source, ref, options)
+	}
+	return ioutil.NopCloser(strings.NewReader("")), nil
+}
+
+func (cli *fakeClient) ImageHistory(_ context.Context, img string) ([]image.HistoryResponseItem, error) {
+	if cli.imageHistoryFunc != nil {
+		return cli.imageHistoryFunc(img)
+	}
+	return []image.HistoryResponseItem{{ID: img, Created: time.Now().Unix()}}, nil
+}

+ 2 - 2
cli/command/image/history.go

@@ -19,7 +19,7 @@ type historyOptions struct {
 }
 
 // NewHistoryCommand creates a new `docker history` command
-func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
 	var opts historyOptions
 
 	cmd := &cobra.Command{
@@ -42,7 +42,7 @@ func NewHistoryCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runHistory(dockerCli *command.DockerCli, opts historyOptions) error {
+func runHistory(dockerCli command.Cli, opts historyOptions) error {
 	ctx := context.Background()
 
 	history, err := dockerCli.Client().ImageHistory(ctx, opts.image)

+ 108 - 0
cli/command/image/history_test.go

@@ -0,0 +1,108 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/docker/docker/api/types/image"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewHistoryCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name             string
+		args             []string
+		expectedError    string
+		imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error)
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{},
+			expectedError: "requires exactly 1 argument(s).",
+		},
+		{
+			name:          "client-error",
+			args:          []string{"image:tag"},
+			expectedError: "something went wrong",
+			imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) {
+				return []image.HistoryResponseItem{{}}, errors.Errorf("something went wrong")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewHistoryCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name             string
+		args             []string
+		outputRegex      string
+		imageHistoryFunc func(img string) ([]image.HistoryResponseItem, error)
+	}{
+		{
+			name: "simple",
+			args: []string{"image:tag"},
+			imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) {
+				return []image.HistoryResponseItem{{
+					ID:      "1234567890123456789",
+					Created: time.Now().Unix(),
+				}}, nil
+			},
+		},
+		{
+			name: "quiet",
+			args: []string{"--quiet", "image:tag"},
+		},
+		// TODO: This test is failing since the output does not contain an RFC3339 date
+		//{
+		//	name:        "non-human",
+		//	args:        []string{"--human=false", "image:tag"},
+		//	outputRegex: "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", // RFC3339 date format match
+		//},
+		{
+			name:        "non-human-header",
+			args:        []string{"--human=false", "image:tag"},
+			outputRegex: "CREATED\\sAT",
+		},
+		{
+			name: "quiet-no-trunc",
+			args: []string{"--quiet", "--no-trunc", "image:tag"},
+			imageHistoryFunc: func(img string) ([]image.HistoryResponseItem, error) {
+				return []image.HistoryResponseItem{{
+					ID:      "1234567890123456789",
+					Created: time.Now().Unix(),
+				}}, nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewHistoryCommand(test.NewFakeCli(&fakeClient{imageHistoryFunc: tc.imageHistoryFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		if tc.outputRegex == "" {
+			expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("history-command-success.%s.golden", tc.name))[:])
+			testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+		} else {
+			match, _ := regexp.MatchString(tc.outputRegex, actual)
+			assert.Equal(t, match, true)
+		}
+	}
+}

+ 2 - 2
cli/command/image/import.go

@@ -23,7 +23,7 @@ type importOptions struct {
 }
 
 // NewImportCommand creates a new `docker import` command
-func NewImportCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewImportCommand(dockerCli command.Cli) *cobra.Command {
 	var opts importOptions
 
 	cmd := &cobra.Command{
@@ -48,7 +48,7 @@ func NewImportCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runImport(dockerCli *command.DockerCli, opts importOptions) error {
+func runImport(dockerCli command.Cli, opts importOptions) error {
 	var (
 		in      io.Reader
 		srcName = opts.source

+ 100 - 0
cli/command/image/import_test.go

@@ -0,0 +1,100 @@
+package image
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewImportCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		expectedError   string
+		imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{},
+			expectedError: "requires at least 1 argument(s).",
+		},
+		{
+			name:          "import-failed",
+			args:          []string{"testdata/import-command-success.input.txt"},
+			expectedError: "something went wrong",
+			imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
+				return nil, errors.Errorf("something went wrong")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewImportCommandInvalidFile(t *testing.T) {
+	cmd := NewImportCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
+	cmd.SetOutput(ioutil.Discard)
+	cmd.SetArgs([]string{"testdata/import-command-success.unexistent-file"})
+	testutil.ErrorContains(t, cmd.Execute(), "testdata/import-command-success.unexistent-file")
+}
+
+func TestNewImportCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		imageImportFunc func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error)
+	}{
+		{
+			name: "simple",
+			args: []string{"testdata/import-command-success.input.txt"},
+		},
+		{
+			name: "terminal-source",
+			args: []string{"-"},
+		},
+		{
+			name: "double",
+			args: []string{"-", "image:local"},
+			imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
+				assert.Equal(t, ref, "image:local")
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+		},
+		{
+			name: "message",
+			args: []string{"--message", "test message", "-"},
+			imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
+				assert.Equal(t, options.Message, "test message")
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+		},
+		{
+			name: "change",
+			args: []string{"--change", "ENV DEBUG true", "-"},
+			imageImportFunc: func(source types.ImageImportSource, ref string, options types.ImageImportOptions) (io.ReadCloser, error) {
+				assert.Equal(t, options.Changes[0], "ENV DEBUG true")
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewImportCommand(test.NewFakeCli(&fakeClient{imageImportFunc: tc.imageImportFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.NoError(t, cmd.Execute())
+	}
+}

+ 2 - 2
cli/command/image/inspect.go

@@ -15,7 +15,7 @@ type inspectOptions struct {
 }
 
 // newInspectCommand creates a new cobra.Command for `docker image inspect`
-func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newInspectCommand(dockerCli command.Cli) *cobra.Command {
 	var opts inspectOptions
 
 	cmd := &cobra.Command{
@@ -33,7 +33,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
+func runInspect(dockerCli command.Cli, opts inspectOptions) error {
 	client := dockerCli.Client()
 	ctx := context.Background()
 

+ 92 - 0
cli/command/image/inspect_test.go

@@ -0,0 +1,92 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewInspectCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		expectedError string
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{},
+			expectedError: "requires at least 1 argument(s).",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newInspectCommand(test.NewFakeCli(&fakeClient{}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewInspectCommandSuccess(t *testing.T) {
+	imageInspectInvocationCount := 0
+	testCases := []struct {
+		name             string
+		args             []string
+		imageCount       int
+		imageInspectFunc func(image string) (types.ImageInspect, []byte, error)
+	}{
+		{
+			name:       "simple",
+			args:       []string{"image"},
+			imageCount: 1,
+			imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) {
+				imageInspectInvocationCount++
+				assert.Equal(t, image, "image")
+				return types.ImageInspect{}, nil, nil
+			},
+		},
+		{
+			name:       "format",
+			imageCount: 1,
+			args:       []string{"--format='{{.ID}}'", "image"},
+			imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) {
+				imageInspectInvocationCount++
+				return types.ImageInspect{ID: image}, nil, nil
+			},
+		},
+		{
+			name:       "simple-many",
+			args:       []string{"image1", "image2"},
+			imageCount: 2,
+			imageInspectFunc: func(image string) (types.ImageInspect, []byte, error) {
+				imageInspectInvocationCount++
+				if imageInspectInvocationCount == 1 {
+					assert.Equal(t, image, "image1")
+				} else {
+					assert.Equal(t, image, "image2")
+				}
+				return types.ImageInspect{}, nil, nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		imageInspectInvocationCount = 0
+		buf := new(bytes.Buffer)
+		cmd := newInspectCommand(test.NewFakeCli(&fakeClient{imageInspectFunc: tc.imageInspectFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("inspect-command-success.%s.golden", tc.name))[:])
+		testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+		assert.Equal(t, tc.imageCount, imageInspectInvocationCount)
+	}
+}

+ 3 - 3
cli/command/image/list.go

@@ -23,7 +23,7 @@ type imagesOptions struct {
 }
 
 // NewImagesCommand creates a new `docker images` command
-func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewImagesCommand(dockerCli command.Cli) *cobra.Command {
 	opts := imagesOptions{filter: opts.NewFilterOpt()}
 
 	cmd := &cobra.Command{
@@ -50,14 +50,14 @@ func NewImagesCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newListCommand(dockerCli command.Cli) *cobra.Command {
 	cmd := *NewImagesCommand(dockerCli)
 	cmd.Aliases = []string{"images", "list"}
 	cmd.Use = "ls [OPTIONS] [REPOSITORY[:TAG]]"
 	return &cmd
 }
 
-func runImages(dockerCli *command.DockerCli, opts imagesOptions) error {
+func runImages(dockerCli command.Cli, opts imagesOptions) error {
 	ctx := context.Background()
 
 	filters := opts.filter.Value()

+ 102 - 0
cli/command/image/list_test.go

@@ -0,0 +1,102 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/config/configfile"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewImagesCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		expectedError string
+		imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error)
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{"arg1", "arg2"},
+			expectedError: "requires at most 1 argument(s).",
+		},
+		{
+			name:          "failed-list",
+			expectedError: "something went wrong",
+			imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) {
+				return []types.ImageSummary{{}}, errors.Errorf("something went wrong")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		cmd := NewImagesCommand(test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, new(bytes.Buffer)))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewImagesCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		imageFormat   string
+		imageListFunc func(options types.ImageListOptions) ([]types.ImageSummary, error)
+	}{
+		{
+			name: "simple",
+		},
+		{
+			name:        "format",
+			imageFormat: "raw",
+		},
+		{
+			name:        "quiet-format",
+			args:        []string{"-q"},
+			imageFormat: "table",
+		},
+		{
+			name: "match-name",
+			args: []string{"image"},
+			imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) {
+				assert.Equal(t, options.Filters.Get("reference")[0], "image")
+				return []types.ImageSummary{{}}, nil
+			},
+		},
+		{
+			name: "filters",
+			args: []string{"--filter", "name=value"},
+			imageListFunc: func(options types.ImageListOptions) ([]types.ImageSummary, error) {
+				assert.Equal(t, options.Filters.Get("name")[0], "value")
+				return []types.ImageSummary{{}}, nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cli := test.NewFakeCli(&fakeClient{imageListFunc: tc.imageListFunc}, buf)
+		cli.SetConfigfile(&configfile.ConfigFile{ImagesFormat: tc.imageFormat})
+		cmd := NewImagesCommand(cli)
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("list-command-success.%s.golden", tc.name))[:])
+		testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+	}
+}
+
+func TestNewListCommandAlias(t *testing.T) {
+	cmd := newListCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
+	assert.Equal(t, cmd.HasAlias("images"), true)
+	assert.Equal(t, cmd.HasAlias("list"), true)
+	assert.Equal(t, cmd.HasAlias("other"), false)
+}

+ 2 - 2
cli/command/image/load.go

@@ -19,7 +19,7 @@ type loadOptions struct {
 }
 
 // NewLoadCommand creates a new `docker load` command
-func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewLoadCommand(dockerCli command.Cli) *cobra.Command {
 	var opts loadOptions
 
 	cmd := &cobra.Command{
@@ -39,7 +39,7 @@ func NewLoadCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runLoad(dockerCli *command.DockerCli, opts loadOptions) error {
+func runLoad(dockerCli command.Cli, opts loadOptions) error {
 
 	var input io.Reader = dockerCli.In()
 	if opts.input != "" {

+ 106 - 0
cli/command/image/load_test.go

@@ -0,0 +1,106 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewLoadCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		isTerminalIn  bool
+		expectedError string
+		imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error)
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{"arg"},
+			expectedError: "accepts no argument(s).",
+		},
+		{
+			name:          "input-to-terminal",
+			isTerminalIn:  true,
+			expectedError: "requested load from stdin, but stdin is empty",
+		},
+		{
+			name:          "pull-error",
+			expectedError: "something went wrong",
+			imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
+				return types.ImageLoadResponse{}, errors.Errorf("something went wrong")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		cli := test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, new(bytes.Buffer))
+		cli.In().SetIsTerminal(tc.isTerminalIn)
+		cmd := NewLoadCommand(cli)
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewLoadCommandInvalidInput(t *testing.T) {
+	expectedError := "open *"
+	cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
+	cmd.SetOutput(ioutil.Discard)
+	cmd.SetArgs([]string{"--input", "*"})
+	err := cmd.Execute()
+	assert.NotNil(t, err)
+	assert.Contains(t, err.Error(), expectedError)
+}
+
+func TestNewLoadCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		imageLoadFunc func(input io.Reader, quiet bool) (types.ImageLoadResponse, error)
+	}{
+		{
+			name: "simple",
+			imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
+				return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil
+			},
+		},
+		{
+			name: "json",
+			imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
+				json := "{\"ID\": \"1\"}"
+				return types.ImageLoadResponse{
+					Body: ioutil.NopCloser(strings.NewReader(json)),
+					JSON: true,
+				}, nil
+			},
+		},
+		{
+			name: "input-file",
+			args: []string{"--input", "testdata/load-command-success.input.txt"},
+			imageLoadFunc: func(input io.Reader, quiet bool) (types.ImageLoadResponse, error) {
+				return types.ImageLoadResponse{Body: ioutil.NopCloser(strings.NewReader("Success"))}, nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewLoadCommand(test.NewFakeCli(&fakeClient{imageLoadFunc: tc.imageLoadFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("load-command-success.%s.golden", tc.name))[:])
+		testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+	}
+}

+ 3 - 3
cli/command/image/prune.go

@@ -19,7 +19,7 @@ type pruneOptions struct {
 }
 
 // NewPruneCommand returns a new cobra prune command for images
-func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
 	opts := pruneOptions{filter: opts.NewFilterOpt()}
 
 	cmd := &cobra.Command{
@@ -55,7 +55,7 @@ Are you sure you want to continue?`
 Are you sure you want to continue?`
 )
 
-func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
+func runPrune(dockerCli command.Cli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
 	pruneFilters := opts.filter.Value()
 	pruneFilters.Add("dangling", fmt.Sprintf("%v", !opts.all))
 	pruneFilters = command.PruneFilters(dockerCli, pruneFilters)
@@ -90,6 +90,6 @@ func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed u
 
 // RunPrune calls the Image Prune API
 // This returns the amount of space reclaimed and a detailed output string
-func RunPrune(dockerCli *command.DockerCli, all bool, filter opts.FilterOpt) (uint64, string, error) {
+func RunPrune(dockerCli command.Cli, all bool, filter opts.FilterOpt) (uint64, string, error) {
 	return runPrune(dockerCli, pruneOptions{force: true, all: all, filter: filter})
 }

+ 100 - 0
cli/command/image/prune_test.go

@@ -0,0 +1,100 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewPruneCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		expectedError   string
+		imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error)
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{"something"},
+			expectedError: "accepts no argument(s).",
+		},
+		{
+			name:          "prune-error",
+			args:          []string{"--force"},
+			expectedError: "something went wrong",
+			imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
+				return types.ImagesPruneReport{}, errors.Errorf("something went wrong")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
+			imagesPruneFunc: tc.imagesPruneFunc,
+		}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewPruneCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		imagesPruneFunc func(pruneFilter filters.Args) (types.ImagesPruneReport, error)
+	}{
+		{
+			name: "all",
+			args: []string{"--all"},
+			imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
+				assert.Equal(t, pruneFilter.Get("dangling")[0], "false")
+				return types.ImagesPruneReport{}, nil
+			},
+		},
+		{
+			name: "force-deleted",
+			args: []string{"--force"},
+			imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
+				assert.Equal(t, pruneFilter.Get("dangling")[0], "true")
+				return types.ImagesPruneReport{
+					ImagesDeleted:  []types.ImageDeleteResponseItem{{Deleted: "image1"}},
+					SpaceReclaimed: 1,
+				}, nil
+			},
+		},
+		{
+			name: "force-untagged",
+			args: []string{"--force"},
+			imagesPruneFunc: func(pruneFilter filters.Args) (types.ImagesPruneReport, error) {
+				assert.Equal(t, pruneFilter.Get("dangling")[0], "true")
+				return types.ImagesPruneReport{
+					ImagesDeleted:  []types.ImageDeleteResponseItem{{Untagged: "image1"}},
+					SpaceReclaimed: 2,
+				}, nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPruneCommand(test.NewFakeCli(&fakeClient{
+			imagesPruneFunc: tc.imagesPruneFunc,
+		}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("prune-command-success.%s.golden", tc.name))[:])
+		testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+	}
+}

+ 2 - 2
cli/command/image/pull.go

@@ -19,7 +19,7 @@ type pullOptions struct {
 }
 
 // NewPullCommand creates a new `docker pull` command
-func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewPullCommand(dockerCli command.Cli) *cobra.Command {
 	var opts pullOptions
 
 	cmd := &cobra.Command{
@@ -40,7 +40,7 @@ func NewPullCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
+func runPull(dockerCli command.Cli, opts pullOptions) error {
 	distributionRef, err := reference.ParseNormalizedNamed(opts.remote)
 	if err != nil {
 		return err

+ 85 - 0
cli/command/image/pull_test.go

@@ -0,0 +1,85 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/docker/docker/registry"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/net/context"
+)
+
+func TestNewPullCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		expectedError   string
+		trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named,
+			authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error
+	}{
+		{
+			name:          "wrong-args",
+			expectedError: "requires exactly 1 argument(s).",
+			args:          []string{},
+		},
+		{
+			name:          "invalid-name",
+			expectedError: "invalid reference format: repository name must be lowercase",
+			args:          []string{"UPPERCASE_REPO"},
+		},
+		{
+			name:          "all-tags-with-tag",
+			expectedError: "tag can't be used with --all-tags/-a",
+			args:          []string{"--all-tags", "image:tag"},
+		},
+		{
+			name:          "pull-error",
+			args:          []string{"--disable-content-trust=false", "image:tag"},
+			expectedError: "you are not authorized to perform this operation: server returned 401.",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewPullCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		trustedPullFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named,
+			authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error
+	}{
+		{
+			name: "simple",
+			args: []string{"image:tag"},
+		},
+		{
+			name: "simple-no-tag",
+			args: []string{"image"},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPullCommand(test.NewFakeCli(&fakeClient{}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("pull-command-success.%s.golden", tc.name))[:])
+		testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+	}
+}

+ 2 - 2
cli/command/image/push.go

@@ -12,7 +12,7 @@ import (
 )
 
 // NewPushCommand creates a new `docker push` command
-func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewPushCommand(dockerCli command.Cli) *cobra.Command {
 	cmd := &cobra.Command{
 		Use:   "push [OPTIONS] NAME[:TAG]",
 		Short: "Push an image or a repository to a registry",
@@ -29,7 +29,7 @@ func NewPushCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runPush(dockerCli *command.DockerCli, remote string) error {
+func runPush(dockerCli command.Cli, remote string) error {
 	ref, err := reference.ParseNormalizedNamed(remote)
 	if err != nil {
 		return err

+ 84 - 0
cli/command/image/push_test.go

@@ -0,0 +1,84 @@
+package image
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"strings"
+	"testing"
+
+	"github.com/docker/distribution/reference"
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/registry"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"golang.org/x/net/context"
+)
+
+func TestNewPushCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		expectedError string
+		imagePushFunc func(ref string, options types.ImagePushOptions) (io.ReadCloser, error)
+	}{
+		{
+			name:          "wrong-args",
+			args:          []string{},
+			expectedError: "requires exactly 1 argument(s).",
+		},
+		{
+			name:          "invalid-name",
+			args:          []string{"UPPERCASE_REPO"},
+			expectedError: "invalid reference format: repository name must be lowercase",
+		},
+		{
+			name:          "push-failed",
+			args:          []string{"image:repo"},
+			expectedError: "Failed to push",
+			imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
+				return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("Failed to push")
+			},
+		},
+		{
+			name:          "trust-error",
+			args:          []string{"--disable-content-trust=false", "image:repo"},
+			expectedError: "you are not authorized to perform this operation: server returned 401.",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPushCommand(test.NewFakeCli(&fakeClient{imagePushFunc: tc.imagePushFunc}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewPushCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		trustedPushFunc func(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo,
+			ref reference.Named, authConfig types.AuthConfig,
+			requestPrivilege types.RequestPrivilegeFunc) error
+	}{
+		{
+			name: "simple",
+			args: []string{"image:tag"},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPushCommand(test.NewFakeCli(&fakeClient{
+			imagePushFunc: func(ref string, options types.ImagePushOptions) (io.ReadCloser, error) {
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+		}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.NoError(t, cmd.Execute())
+	}
+}

+ 3 - 3
cli/command/image/remove.go

@@ -19,7 +19,7 @@ type removeOptions struct {
 }
 
 // NewRemoveCommand creates a new `docker remove` command
-func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewRemoveCommand(dockerCli command.Cli) *cobra.Command {
 	var opts removeOptions
 
 	cmd := &cobra.Command{
@@ -39,14 +39,14 @@ func NewRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
 	cmd := *NewRemoveCommand(dockerCli)
 	cmd.Aliases = []string{"rmi", "remove"}
 	cmd.Use = "rm [OPTIONS] IMAGE [IMAGE...]"
 	return &cmd
 }
 
-func runRemove(dockerCli *command.DockerCli, opts removeOptions, images []string) error {
+func runRemove(dockerCli command.Cli, opts removeOptions, images []string) error {
 	client := dockerCli.Client()
 	ctx := context.Background()
 

+ 103 - 0
cli/command/image/remove_test.go

@@ -0,0 +1,103 @@
+package image
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil"
+	"github.com/docker/docker/pkg/testutil/golden"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewRemoveCommandAlias(t *testing.T) {
+	cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, new(bytes.Buffer)))
+	assert.Equal(t, cmd.HasAlias("rmi"), true)
+	assert.Equal(t, cmd.HasAlias("remove"), true)
+	assert.Equal(t, cmd.HasAlias("other"), false)
+}
+
+func TestNewRemoveCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		expectedError   string
+		imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
+	}{
+		{
+			name:          "wrong args",
+			expectedError: "requires at least 1 argument(s).",
+		},
+		{
+			name:          "ImageRemove fail",
+			args:          []string{"arg1"},
+			expectedError: "error removing image",
+			imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
+				assert.Equal(t, options.Force, false)
+				assert.Equal(t, options.PruneChildren, true)
+				return []types.ImageDeleteResponseItem{}, errors.Errorf("error removing image")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
+			imageRemoveFunc: tc.imageRemoveFunc,
+		}, new(bytes.Buffer)))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewRemoveCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		name            string
+		args            []string
+		imageRemoveFunc func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error)
+	}{
+		{
+			name: "Image Deleted",
+			args: []string{"image1"},
+			imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
+				assert.Equal(t, image, "image1")
+				return []types.ImageDeleteResponseItem{{Deleted: image}}, nil
+			},
+		},
+		{
+			name: "Image Untagged",
+			args: []string{"image1"},
+			imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
+				assert.Equal(t, image, "image1")
+				return []types.ImageDeleteResponseItem{{Untagged: image}}, nil
+			},
+		},
+		{
+			name: "Image Deleted and Untagged",
+			args: []string{"image1", "image2"},
+			imageRemoveFunc: func(image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) {
+				if image == "image1" {
+					return []types.ImageDeleteResponseItem{{Untagged: image}}, nil
+				}
+				return []types.ImageDeleteResponseItem{{Deleted: image}}, nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewRemoveCommand(test.NewFakeCli(&fakeClient{
+			imageRemoveFunc: tc.imageRemoveFunc,
+		}, buf))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.NoError(t, cmd.Execute())
+		err := cmd.Execute()
+		assert.NoError(t, err)
+		actual := buf.String()
+		expected := string(golden.Get(t, []byte(actual), fmt.Sprintf("remove-command-success.%s.golden", tc.name))[:])
+		testutil.EqualNormalizedString(t, testutil.RemoveSpace, actual, expected)
+	}
+}

+ 2 - 2
cli/command/image/save.go

@@ -16,7 +16,7 @@ type saveOptions struct {
 }
 
 // NewSaveCommand creates a new `docker save` command
-func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewSaveCommand(dockerCli command.Cli) *cobra.Command {
 	var opts saveOptions
 
 	cmd := &cobra.Command{
@@ -36,7 +36,7 @@ func NewSaveCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runSave(dockerCli *command.DockerCli, opts saveOptions) error {
+func runSave(dockerCli command.Cli, opts saveOptions) error {
 	if opts.output == "" && dockerCli.Out().IsTerminal() {
 		return errors.New("Cowardly refusing to save to a terminal. Use the -o flag or redirect.")
 	}

+ 98 - 0
cli/command/image/save_test.go

@@ -0,0 +1,98 @@
+package image
+
+import (
+	"bytes"
+	"io"
+	"io/ioutil"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/pkg/errors"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewSaveCommandErrors(t *testing.T) {
+	testCases := []struct {
+		name          string
+		args          []string
+		isTerminal    bool
+		expectedError string
+		imageSaveFunc func(images []string) (io.ReadCloser, error)
+	}{
+		{
+			name:          "wrong args",
+			args:          []string{},
+			expectedError: "requires at least 1 argument(s).",
+		},
+		{
+			name:          "output to terminal",
+			args:          []string{"output", "file", "arg1"},
+			isTerminal:    true,
+			expectedError: "Cowardly refusing to save to a terminal. Use the -o flag or redirect.",
+		},
+		{
+			name:          "ImageSave fail",
+			args:          []string{"arg1"},
+			isTerminal:    false,
+			expectedError: "error saving image",
+			imageSaveFunc: func(images []string) (io.ReadCloser, error) {
+				return ioutil.NopCloser(strings.NewReader("")), errors.Errorf("error saving image")
+			},
+		},
+	}
+	for _, tc := range testCases {
+		cli := test.NewFakeCli(&fakeClient{imageSaveFunc: tc.imageSaveFunc}, new(bytes.Buffer))
+		cli.Out().SetIsTerminal(tc.isTerminal)
+		cmd := NewSaveCommand(cli)
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNewSaveCommandSuccess(t *testing.T) {
+	testCases := []struct {
+		args          []string
+		isTerminal    bool
+		imageSaveFunc func(images []string) (io.ReadCloser, error)
+		deferredFunc  func()
+	}{
+		{
+			args:       []string{"-o", "save_tmp_file", "arg1"},
+			isTerminal: true,
+			imageSaveFunc: func(images []string) (io.ReadCloser, error) {
+				assert.Equal(t, len(images), 1)
+				assert.Equal(t, images[0], "arg1")
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+			deferredFunc: func() {
+				os.Remove("save_tmp_file")
+			},
+		},
+		{
+			args:       []string{"arg1", "arg2"},
+			isTerminal: false,
+			imageSaveFunc: func(images []string) (io.ReadCloser, error) {
+				assert.Equal(t, len(images), 2)
+				assert.Equal(t, images[0], "arg1")
+				assert.Equal(t, images[1], "arg2")
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		cmd := NewSaveCommand(test.NewFakeCli(&fakeClient{
+			imageSaveFunc: func(images []string) (io.ReadCloser, error) {
+				return ioutil.NopCloser(strings.NewReader("")), nil
+			},
+		}, new(bytes.Buffer)))
+		cmd.SetOutput(ioutil.Discard)
+		cmd.SetArgs(tc.args)
+		assert.NoError(t, cmd.Execute())
+		if tc.deferredFunc != nil {
+			tc.deferredFunc()
+		}
+	}
+}

+ 2 - 2
cli/command/image/tag.go

@@ -14,7 +14,7 @@ type tagOptions struct {
 }
 
 // NewTagCommand creates a new `docker tag` command
-func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewTagCommand(dockerCli command.Cli) *cobra.Command {
 	var opts tagOptions
 
 	cmd := &cobra.Command{
@@ -34,7 +34,7 @@ func NewTagCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runTag(dockerCli *command.DockerCli, opts tagOptions) error {
+func runTag(dockerCli command.Cli, opts tagOptions) error {
 	ctx := context.Background()
 
 	return dockerCli.Client().ImageTag(ctx, opts.image, opts.name)

+ 43 - 0
cli/command/image/tag_test.go

@@ -0,0 +1,43 @@
+package image
+
+import (
+	"bytes"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCliNewTagCommandErrors(t *testing.T) {
+	testCases := [][]string{
+		{},
+		{"image1"},
+		{"image1", "image2", "image3"},
+	}
+	expectedError := "\"tag\" requires exactly 2 argument(s)."
+	buf := new(bytes.Buffer)
+	for _, args := range testCases {
+		cmd := NewTagCommand(test.NewFakeCli(&fakeClient{}, buf))
+		cmd.SetArgs(args)
+		cmd.SetOutput(ioutil.Discard)
+		assert.Error(t, cmd.Execute(), expectedError)
+	}
+}
+
+func TestCliNewTagCommand(t *testing.T) {
+	buf := new(bytes.Buffer)
+	cmd := NewTagCommand(
+		test.NewFakeCli(&fakeClient{
+			imageTagFunc: func(image string, ref string) error {
+				assert.Equal(t, image, "image1")
+				assert.Equal(t, ref, "image2")
+				return nil
+			},
+		}, buf))
+	cmd.SetArgs([]string{"image1", "image2"})
+	cmd.SetOutput(ioutil.Discard)
+	assert.NoError(t, cmd.Execute())
+	value, _ := cmd.Flags().GetBool("interspersed")
+	assert.Equal(t, value, false)
+}

+ 1 - 0
cli/command/image/testdata/history-command-success.quiet-no-trunc.golden

@@ -0,0 +1 @@
+1234567890123456789

+ 1 - 0
cli/command/image/testdata/history-command-success.quiet.golden

@@ -0,0 +1 @@
+tag

+ 2 - 0
cli/command/image/testdata/history-command-success.simple.golden

@@ -0,0 +1,2 @@
+IMAGE               CREATED                  CREATED BY          SIZE                COMMENT
+123456789012        Less than a second ago                       0B                  

+ 1 - 0
cli/command/image/testdata/import-command-success.input.txt

@@ -0,0 +1 @@
+file input test

+ 1 - 0
cli/command/image/testdata/inspect-command-success.format.golden

@@ -0,0 +1 @@
+'image'

+ 50 - 0
cli/command/image/testdata/inspect-command-success.simple-many.golden

@@ -0,0 +1,50 @@
+[
+    {
+        "Id": "",
+        "RepoTags": null,
+        "RepoDigests": null,
+        "Parent": "",
+        "Comment": "",
+        "Created": "",
+        "Container": "",
+        "ContainerConfig": null,
+        "DockerVersion": "",
+        "Author": "",
+        "Config": null,
+        "Architecture": "",
+        "Os": "",
+        "Size": 0,
+        "VirtualSize": 0,
+        "GraphDriver": {
+            "Data": null,
+            "Name": ""
+        },
+        "RootFS": {
+            "Type": ""
+        }
+    },
+    {
+        "Id": "",
+        "RepoTags": null,
+        "RepoDigests": null,
+        "Parent": "",
+        "Comment": "",
+        "Created": "",
+        "Container": "",
+        "ContainerConfig": null,
+        "DockerVersion": "",
+        "Author": "",
+        "Config": null,
+        "Architecture": "",
+        "Os": "",
+        "Size": 0,
+        "VirtualSize": 0,
+        "GraphDriver": {
+            "Data": null,
+            "Name": ""
+        },
+        "RootFS": {
+            "Type": ""
+        }
+    }
+]

+ 26 - 0
cli/command/image/testdata/inspect-command-success.simple.golden

@@ -0,0 +1,26 @@
+[
+    {
+        "Id": "",
+        "RepoTags": null,
+        "RepoDigests": null,
+        "Parent": "",
+        "Comment": "",
+        "Created": "",
+        "Container": "",
+        "ContainerConfig": null,
+        "DockerVersion": "",
+        "Author": "",
+        "Config": null,
+        "Architecture": "",
+        "Os": "",
+        "Size": 0,
+        "VirtualSize": 0,
+        "GraphDriver": {
+            "Data": null,
+            "Name": ""
+        },
+        "RootFS": {
+            "Type": ""
+        }
+    }
+]

+ 1 - 0
cli/command/image/testdata/list-command-success.filters.golden

@@ -0,0 +1 @@
+REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

+ 0 - 0
cli/command/image/testdata/list-command-success.format.golden


+ 1 - 0
cli/command/image/testdata/list-command-success.match-name.golden

@@ -0,0 +1 @@
+REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

+ 0 - 0
cli/command/image/testdata/list-command-success.quiet-format.golden


+ 1 - 0
cli/command/image/testdata/list-command-success.simple.golden

@@ -0,0 +1 @@
+REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE

+ 1 - 0
cli/command/image/testdata/load-command-success.input-file.golden

@@ -0,0 +1 @@
+Success

+ 1 - 0
cli/command/image/testdata/load-command-success.input.txt

@@ -0,0 +1 @@
+file input test

+ 1 - 0
cli/command/image/testdata/load-command-success.json.golden

@@ -0,0 +1 @@
+1: 

+ 1 - 0
cli/command/image/testdata/load-command-success.simple.golden

@@ -0,0 +1 @@
+Success

+ 2 - 0
cli/command/image/testdata/prune-command-success.all.golden

@@ -0,0 +1,2 @@
+WARNING! This will remove all images without at least one container associated to them.
+Are you sure you want to continue? [y/N] Total reclaimed space: 0B

+ 4 - 0
cli/command/image/testdata/prune-command-success.force-deleted.golden

@@ -0,0 +1,4 @@
+Deleted Images:
+deleted: image1
+
+Total reclaimed space: 1B

+ 4 - 0
cli/command/image/testdata/prune-command-success.force-untagged.golden

@@ -0,0 +1,4 @@
+Deleted Images:
+untagged: image1
+
+Total reclaimed space: 2B

+ 1 - 0
cli/command/image/testdata/pull-command-success.simple-no-tag.golden

@@ -0,0 +1 @@
+Using default tag: latest

+ 0 - 0
cli/command/image/testdata/pull-command-success.simple.golden


+ 4 - 0
cli/command/image/testdata/remove-command-success.Image Deleted and Untagged.golden

@@ -0,0 +1,4 @@
+Untagged: image1
+Deleted: image2
+Untagged: image1
+Deleted: image2

+ 2 - 0
cli/command/image/testdata/remove-command-success.Image Deleted.golden

@@ -0,0 +1,2 @@
+Deleted: image1
+Deleted: image1

+ 2 - 0
cli/command/image/testdata/remove-command-success.Image Untagged.golden

@@ -0,0 +1,2 @@
+Untagged: image1
+Untagged: image1

+ 7 - 7
cli/command/image/trust.go

@@ -29,7 +29,7 @@ type target struct {
 }
 
 // trustedPush handles content trust pushing of an image
-func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
+func trustedPush(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
 	responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref, requestPrivilege)
 	if err != nil {
 		return err
@@ -41,7 +41,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
 }
 
 // PushTrustedReference pushes a canonical reference to the trust server.
-func PushTrustedReference(cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error {
+func PushTrustedReference(cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, in io.Reader) error {
 	// If it is a trusted push we would like to find the target entry which match the
 	// tag provided in the function and then do an AddTarget later.
 	target := &client.Target{}
@@ -202,7 +202,7 @@ func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.T
 }
 
 // imagePushPrivileged push the image
-func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) {
+func imagePushPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref reference.Named, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) {
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
 	if err != nil {
 		return nil, err
@@ -216,7 +216,7 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig
 }
 
 // trustedPull handles content trust pulling of an image
-func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
+func trustedPull(ctx context.Context, cli command.Cli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
 	var refs []target
 
 	notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
@@ -295,7 +295,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
 }
 
 // imagePullPrivileged pulls the image and displays it to the output
-func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
+func imagePullPrivileged(ctx context.Context, cli command.Cli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
 
 	encodedAuth, err := command.EncodeAuthToBase64(authConfig)
 	if err != nil {
@@ -317,7 +317,7 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig
 }
 
 // TrustedReference returns the canonical trusted reference for an image reference
-func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) {
+func TrustedReference(ctx context.Context, cli command.Cli, ref reference.NamedTagged, rs registry.Service) (reference.Canonical, error) {
 	var (
 		repoInfo *registry.RepositoryInfo
 		err      error
@@ -371,7 +371,7 @@ func convertTarget(t client.Target) (target, error) {
 }
 
 // TagTrusted tags a trusted ref
-func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
+func TagTrusted(ctx context.Context, cli command.Cli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
 	// Use familiar references when interacting with client and output
 	familiarRef := reference.FamiliarString(ref)
 	trustedFamiliarRef := reference.FamiliarString(trustedRef)

+ 5 - 35
cli/command/in.go

@@ -1,20 +1,16 @@
 package command
 
 import (
+	"errors"
+	"github.com/docker/docker/pkg/term"
 	"io"
-	"os"
 	"runtime"
-
-	"github.com/docker/docker/pkg/term"
-	"github.com/pkg/errors"
 )
 
 // InStream is an input stream used by the DockerCli to read user input
 type InStream struct {
-	in         io.ReadCloser
-	fd         uintptr
-	isTerminal bool
-	state      *term.State
+	CommonStream
+	in io.ReadCloser
 }
 
 func (i *InStream) Read(p []byte) (int, error) {
@@ -26,32 +22,6 @@ func (i *InStream) Close() error {
 	return i.in.Close()
 }
 
-// FD returns the file descriptor number for this stream
-func (i *InStream) FD() uintptr {
-	return i.fd
-}
-
-// IsTerminal returns true if this stream is connected to a terminal
-func (i *InStream) IsTerminal() bool {
-	return i.isTerminal
-}
-
-// SetRawTerminal sets raw mode on the input terminal
-func (i *InStream) SetRawTerminal() (err error) {
-	if os.Getenv("NORAW") != "" || !i.isTerminal {
-		return nil
-	}
-	i.state, err = term.SetRawTerminal(i.fd)
-	return err
-}
-
-// RestoreTerminal restores normal mode to the terminal
-func (i *InStream) RestoreTerminal() {
-	if i.state != nil {
-		term.RestoreTerminal(i.fd, i.state)
-	}
-}
-
 // CheckTty checks if we are trying to attach to a container tty
 // from a non-tty client input stream, and if so, returns an error.
 func (i *InStream) CheckTty(attachStdin, ttyMode bool) error {
@@ -71,5 +41,5 @@ func (i *InStream) CheckTty(attachStdin, ttyMode bool) error {
 // NewInStream returns a new InStream object from a ReadCloser
 func NewInStream(in io.ReadCloser) *InStream {
 	fd, isTerminal := term.GetFdInfo(in)
-	return &InStream{in: in, fd: fd, isTerminal: isTerminal}
+	return &InStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, in: in}
 }

+ 4 - 34
cli/command/out.go

@@ -1,52 +1,22 @@
 package command
 
 import (
-	"io"
-	"os"
-
 	"github.com/Sirupsen/logrus"
 	"github.com/docker/docker/pkg/term"
+	"io"
 )
 
 // OutStream is an output stream used by the DockerCli to write normal program
 // output.
 type OutStream struct {
-	out        io.Writer
-	fd         uintptr
-	isTerminal bool
-	state      *term.State
+	CommonStream
+	out io.Writer
 }
 
 func (o *OutStream) Write(p []byte) (int, error) {
 	return o.out.Write(p)
 }
 
-// FD returns the file descriptor number for this stream
-func (o *OutStream) FD() uintptr {
-	return o.fd
-}
-
-// IsTerminal returns true if this stream is connected to a terminal
-func (o *OutStream) IsTerminal() bool {
-	return o.isTerminal
-}
-
-// SetRawTerminal sets raw mode on the output terminal
-func (o *OutStream) SetRawTerminal() (err error) {
-	if os.Getenv("NORAW") != "" || !o.isTerminal {
-		return nil
-	}
-	o.state, err = term.SetRawTerminalOutput(o.fd)
-	return err
-}
-
-// RestoreTerminal restores normal mode to the terminal
-func (o *OutStream) RestoreTerminal() {
-	if o.state != nil {
-		term.RestoreTerminal(o.fd, o.state)
-	}
-}
-
 // GetTtySize returns the height and width in characters of the tty
 func (o *OutStream) GetTtySize() (uint, uint) {
 	if !o.isTerminal {
@@ -65,5 +35,5 @@ func (o *OutStream) GetTtySize() (uint, uint) {
 // NewOutStream returns a new OutStream object from a Writer
 func NewOutStream(out io.Writer) *OutStream {
 	fd, isTerminal := term.GetFdInfo(out)
-	return &OutStream{out: out, fd: fd, isTerminal: isTerminal}
+	return &OutStream{CommonStream: CommonStream{fd: fd, isTerminal: isTerminal}, out: out}
 }

+ 7 - 7
cli/command/registry.go

@@ -21,7 +21,7 @@ import (
 )
 
 // ElectAuthServer returns the default registry to use (by asking the daemon)
-func ElectAuthServer(ctx context.Context, cli *DockerCli) string {
+func ElectAuthServer(ctx context.Context, cli Cli) string {
 	// The daemon `/info` endpoint informs us of the default registry being
 	// used. This is essential in cross-platforms environment, where for
 	// example a Linux client might be interacting with a Windows daemon, hence
@@ -46,7 +46,7 @@ func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
 
 // RegistryAuthenticationPrivilegedFunc returns a RequestPrivilegeFunc from the specified registry index info
 // for the given command.
-func RegistryAuthenticationPrivilegedFunc(cli *DockerCli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
+func RegistryAuthenticationPrivilegedFunc(cli Cli, index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc {
 	return func() (string, error) {
 		fmt.Fprintf(cli.Out(), "\nPlease login prior to %s:\n", cmdName)
 		indexServer := registry.GetAuthConfigKey(index)
@@ -62,7 +62,7 @@ func RegistryAuthenticationPrivilegedFunc(cli *DockerCli, index *registrytypes.I
 // ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the
 // default index, it uses the default index name for the daemon's platform,
 // not the client's platform.
-func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes.IndexInfo) types.AuthConfig {
+func ResolveAuthConfig(ctx context.Context, cli Cli, index *registrytypes.IndexInfo) types.AuthConfig {
 	configKey := index.Name
 	if index.Official {
 		configKey = ElectAuthServer(ctx, cli)
@@ -73,10 +73,10 @@ func ResolveAuthConfig(ctx context.Context, cli *DockerCli, index *registrytypes
 }
 
 // ConfigureAuth returns an AuthConfig from the specified user, password and server.
-func ConfigureAuth(cli *DockerCli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) {
+func ConfigureAuth(cli Cli, flUser, flPassword, serverAddress string, isDefaultRegistry bool) (types.AuthConfig, error) {
 	// On Windows, force the use of the regular OS stdin stream. Fixes #14336/#14210
 	if runtime.GOOS == "windows" {
-		cli.in = NewInStream(os.Stdin)
+		cli.SetIn(NewInStream(os.Stdin))
 	}
 
 	if !isDefaultRegistry {
@@ -160,7 +160,7 @@ func promptWithDefault(out io.Writer, prompt string, configDefault string) {
 }
 
 // RetrieveAuthTokenFromImage retrieves an encoded auth token given a complete image
-func RetrieveAuthTokenFromImage(ctx context.Context, cli *DockerCli, image string) (string, error) {
+func RetrieveAuthTokenFromImage(ctx context.Context, cli Cli, image string) (string, error) {
 	// Retrieve encoded auth token from the image reference
 	authConfig, err := resolveAuthConfigFromImage(ctx, cli, image)
 	if err != nil {
@@ -174,7 +174,7 @@ func RetrieveAuthTokenFromImage(ctx context.Context, cli *DockerCli, image strin
 }
 
 // resolveAuthConfigFromImage retrieves that AuthConfig using the image string
-func resolveAuthConfigFromImage(ctx context.Context, cli *DockerCli, image string) (types.AuthConfig, error) {
+func resolveAuthConfigFromImage(ctx context.Context, cli Cli, image string) (types.AuthConfig, error) {
 	registryRef, err := reference.ParseNormalizedNamed(image)
 	if err != nil {
 		return types.AuthConfig{}, err

+ 44 - 0
cli/command/stream.go

@@ -0,0 +1,44 @@
+package command
+
+import (
+	"github.com/docker/docker/pkg/term"
+	"os"
+)
+
+// CommonStream is an input stream used by the DockerCli to read user input
+type CommonStream struct {
+	fd         uintptr
+	isTerminal bool
+	state      *term.State
+}
+
+// FD returns the file descriptor number for this stream
+func (s *CommonStream) FD() uintptr {
+	return s.fd
+}
+
+// IsTerminal returns true if this stream is connected to a terminal
+func (s *CommonStream) IsTerminal() bool {
+	return s.isTerminal
+}
+
+// SetRawTerminal sets raw mode on the input terminal
+func (s *CommonStream) SetRawTerminal() (err error) {
+	if os.Getenv("NORAW") != "" || !s.isTerminal {
+		return nil
+	}
+	s.state, err = term.SetRawTerminal(s.fd)
+	return err
+}
+
+// RestoreTerminal restores normal mode to the terminal
+func (s *CommonStream) RestoreTerminal() {
+	if s.state != nil {
+		term.RestoreTerminal(s.fd, s.state)
+	}
+}
+
+// SetIsTerminal sets the boolean used for isTerminal
+func (s *CommonStream) SetIsTerminal(isTerminal bool) {
+	s.isTerminal = isTerminal
+}

+ 2 - 1
cli/command/swarm/unlock_test.go

@@ -8,6 +8,7 @@ import (
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/swarm"
+	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/internal/test"
 	"github.com/docker/docker/pkg/testutil"
 	"github.com/pkg/errors"
@@ -96,7 +97,7 @@ func TestSwarmUnlock(t *testing.T) {
 			return nil
 		},
 	}, buf)
-	dockerCli.SetIn(ioutil.NopCloser(strings.NewReader(input)))
+	dockerCli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input))))
 	cmd := newUnlockCommand(dockerCli)
 	assert.NoError(t, cmd.Execute())
 }

+ 3 - 2
cli/command/volume/prune_test.go

@@ -10,6 +10,7 @@ import (
 
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/internal/test"
 	"github.com/docker/docker/pkg/testutil"
 	"github.com/docker/docker/pkg/testutil/golden"
@@ -91,7 +92,7 @@ func TestVolumePrunePromptYes(t *testing.T) {
 			volumePruneFunc: simplePruneFunc,
 		}, buf)
 
-		cli.SetIn(ioutil.NopCloser(strings.NewReader(input)))
+		cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input))))
 		cmd := NewPruneCommand(
 			cli,
 		)
@@ -113,7 +114,7 @@ func TestVolumePrunePromptNo(t *testing.T) {
 			volumePruneFunc: simplePruneFunc,
 		}, buf)
 
-		cli.SetIn(ioutil.NopCloser(strings.NewReader(input)))
+		cli.SetIn(command.NewInStream(ioutil.NopCloser(strings.NewReader(input))))
 		cmd := NewPruneCommand(
 			cli,
 		)

+ 17 - 7
cli/internal/test/cli.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/config/configfile"
+	"github.com/docker/docker/cli/config/credentials"
 	"github.com/docker/docker/client"
 )
 
@@ -15,23 +16,24 @@ type FakeCli struct {
 	command.DockerCli
 	client     client.APIClient
 	configfile *configfile.ConfigFile
-	out        io.Writer
+	out        *command.OutStream
 	err        io.Writer
-	in         io.ReadCloser
+	in         *command.InStream
+	store      credentials.Store
 }
 
 // NewFakeCli returns a Cli backed by the fakeCli
 func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli {
 	return &FakeCli{
 		client: client,
-		out:    out,
+		out:    command.NewOutStream(out),
 		err:    ioutil.Discard,
-		in:     ioutil.NopCloser(strings.NewReader("")),
+		in:     command.NewInStream(ioutil.NopCloser(strings.NewReader(""))),
 	}
 }
 
 // SetIn sets the input of the cli to the specified ReadCloser
-func (c *FakeCli) SetIn(in io.ReadCloser) {
+func (c *FakeCli) SetIn(in *command.InStream) {
 	c.in = in
 }
 
@@ -52,7 +54,7 @@ func (c *FakeCli) Client() client.APIClient {
 
 // Out returns the output stream (stdout) the cli should write on
 func (c *FakeCli) Out() *command.OutStream {
-	return command.NewOutStream(c.out)
+	return c.out
 }
 
 // Err returns the output stream (stderr) the cli should write on
@@ -62,10 +64,18 @@ func (c *FakeCli) Err() io.Writer {
 
 // In returns the input stream the cli will use
 func (c *FakeCli) In() *command.InStream {
-	return command.NewInStream(c.in)
+	return c.in
 }
 
 // ConfigFile returns the cli configfile object (to get client configuration)
 func (c *FakeCli) ConfigFile() *configfile.ConfigFile {
 	return c.configfile
 }
+
+// CredentialsStore returns the fake store the cli will use
+func (c *FakeCli) CredentialsStore(serverAddress string) credentials.Store {
+	if c.store == nil {
+		c.store = NewFakeStore()
+	}
+	return c.store
+}

+ 74 - 0
cli/internal/test/store.go

@@ -0,0 +1,74 @@
+package test
+
+import (
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/config/credentials"
+)
+
+// fake store implements a credentials.Store that only acts as an in memory map
+type fakeStore struct {
+	store      map[string]types.AuthConfig
+	eraseFunc  func(serverAddress string) error
+	getFunc    func(serverAddress string) (types.AuthConfig, error)
+	getAllFunc func() (map[string]types.AuthConfig, error)
+	storeFunc  func(authConfig types.AuthConfig) error
+}
+
+// NewFakeStore creates a new file credentials store.
+func NewFakeStore() credentials.Store {
+	return &fakeStore{store: map[string]types.AuthConfig{}}
+}
+
+func (c *fakeStore) SetStore(store map[string]types.AuthConfig) {
+	c.store = store
+}
+
+func (c *fakeStore) SetEraseFunc(eraseFunc func(string) error) {
+	c.eraseFunc = eraseFunc
+}
+
+func (c *fakeStore) SetGetFunc(getFunc func(string) (types.AuthConfig, error)) {
+	c.getFunc = getFunc
+}
+
+func (c *fakeStore) SetGetAllFunc(getAllFunc func() (map[string]types.AuthConfig, error)) {
+	c.getAllFunc = getAllFunc
+}
+
+func (c *fakeStore) SetStoreFunc(storeFunc func(types.AuthConfig) error) {
+	c.storeFunc = storeFunc
+}
+
+// Erase removes the given credentials from the map store
+func (c *fakeStore) Erase(serverAddress string) error {
+	if c.eraseFunc != nil {
+		return c.eraseFunc(serverAddress)
+	}
+	delete(c.store, serverAddress)
+	return nil
+}
+
+// Get retrieves credentials for a specific server from the map store.
+func (c *fakeStore) Get(serverAddress string) (types.AuthConfig, error) {
+	if c.getFunc != nil {
+		return c.getFunc(serverAddress)
+	}
+	authConfig, _ := c.store[serverAddress]
+	return authConfig, nil
+}
+
+func (c *fakeStore) GetAll() (map[string]types.AuthConfig, error) {
+	if c.getAllFunc != nil {
+		return c.getAllFunc()
+	}
+	return c.store, nil
+}
+
+// Store saves the given credentials in the map store.
+func (c *fakeStore) Store(authConfig types.AuthConfig) error {
+	if c.storeFunc != nil {
+		return c.storeFunc(authConfig)
+	}
+	c.store[authConfig.ServerAddress] = authConfig
+	return nil
+}