github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/command/cli_test.go (about) 1 package command 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "net/http/httptest" 11 "os" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/docker/cli/cli/config" 19 "github.com/docker/cli/cli/config/configfile" 20 "github.com/docker/cli/cli/flags" 21 "github.com/docker/docker/api" 22 "github.com/docker/docker/api/types" 23 "github.com/docker/docker/client" 24 "github.com/pkg/errors" 25 "gotest.tools/v3/assert" 26 "gotest.tools/v3/fs" 27 ) 28 29 func TestNewAPIClientFromFlags(t *testing.T) { 30 host := "unix://path" 31 if runtime.GOOS == "windows" { 32 host = "npipe://./" 33 } 34 opts := &flags.ClientOptions{Hosts: []string{host}} 35 apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) 36 assert.NilError(t, err) 37 assert.Equal(t, apiClient.DaemonHost(), host) 38 assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) 39 } 40 41 func TestNewAPIClientFromFlagsForDefaultSchema(t *testing.T) { 42 host := ":2375" 43 slug := "tcp://localhost" 44 if runtime.GOOS == "windows" { 45 slug = "tcp://127.0.0.1" 46 } 47 opts := &flags.ClientOptions{Hosts: []string{host}} 48 apiClient, err := NewAPIClientFromFlags(opts, &configfile.ConfigFile{}) 49 assert.NilError(t, err) 50 assert.Equal(t, apiClient.DaemonHost(), slug+host) 51 assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) 52 } 53 54 func TestNewAPIClientFromFlagsWithCustomHeaders(t *testing.T) { 55 var received map[string]string 56 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 57 received = map[string]string{ 58 "My-Header": r.Header.Get("My-Header"), 59 "User-Agent": r.Header.Get("User-Agent"), 60 } 61 _, _ = w.Write([]byte("OK")) 62 })) 63 defer ts.Close() 64 host := strings.Replace(ts.URL, "http://", "tcp://", 1) 65 opts := &flags.ClientOptions{Hosts: []string{host}} 66 configFile := &configfile.ConfigFile{ 67 HTTPHeaders: map[string]string{ 68 "My-Header": "Custom-Value", 69 }, 70 } 71 72 apiClient, err := NewAPIClientFromFlags(opts, configFile) 73 assert.NilError(t, err) 74 assert.Equal(t, apiClient.DaemonHost(), host) 75 assert.Equal(t, apiClient.ClientVersion(), api.DefaultVersion) 76 77 // verify User-Agent is not appended to the configfile. see https://github.com/docker/cli/pull/2756 78 assert.DeepEqual(t, configFile.HTTPHeaders, map[string]string{"My-Header": "Custom-Value"}) 79 80 expectedHeaders := map[string]string{ 81 "My-Header": "Custom-Value", 82 "User-Agent": UserAgent(), 83 } 84 _, err = apiClient.Ping(context.Background()) 85 assert.NilError(t, err) 86 assert.DeepEqual(t, received, expectedHeaders) 87 } 88 89 func TestNewAPIClientFromFlagsWithAPIVersionFromEnv(t *testing.T) { 90 customVersion := "v3.3.3" 91 t.Setenv("DOCKER_API_VERSION", customVersion) 92 t.Setenv("DOCKER_HOST", ":2375") 93 94 opts := &flags.ClientOptions{} 95 configFile := &configfile.ConfigFile{} 96 apiclient, err := NewAPIClientFromFlags(opts, configFile) 97 assert.NilError(t, err) 98 assert.Equal(t, apiclient.ClientVersion(), customVersion) 99 } 100 101 type fakeClient struct { 102 client.Client 103 pingFunc func() (types.Ping, error) 104 version string 105 negotiated bool 106 } 107 108 func (c *fakeClient) Ping(_ context.Context) (types.Ping, error) { 109 return c.pingFunc() 110 } 111 112 func (c *fakeClient) ClientVersion() string { 113 return c.version 114 } 115 116 func (c *fakeClient) NegotiateAPIVersionPing(types.Ping) { 117 c.negotiated = true 118 } 119 120 func TestInitializeFromClient(t *testing.T) { 121 const defaultVersion = "v1.55" 122 123 testcases := []struct { 124 doc string 125 pingFunc func() (types.Ping, error) 126 expectedServer ServerInfo 127 negotiated bool 128 }{ 129 { 130 doc: "successful ping", 131 pingFunc: func() (types.Ping, error) { 132 return types.Ping{Experimental: true, OSType: "linux", APIVersion: "v1.30"}, nil 133 }, 134 expectedServer: ServerInfo{HasExperimental: true, OSType: "linux"}, 135 negotiated: true, 136 }, 137 { 138 doc: "failed ping, no API version", 139 pingFunc: func() (types.Ping, error) { 140 return types.Ping{}, errors.New("failed") 141 }, 142 expectedServer: ServerInfo{HasExperimental: true}, 143 }, 144 { 145 doc: "failed ping, with API version", 146 pingFunc: func() (types.Ping, error) { 147 return types.Ping{APIVersion: "v1.33"}, errors.New("failed") 148 }, 149 expectedServer: ServerInfo{HasExperimental: true}, 150 negotiated: true, 151 }, 152 } 153 154 for _, testcase := range testcases { 155 testcase := testcase 156 t.Run(testcase.doc, func(t *testing.T) { 157 apiclient := &fakeClient{ 158 pingFunc: testcase.pingFunc, 159 version: defaultVersion, 160 } 161 162 cli := &DockerCli{client: apiclient} 163 err := cli.Initialize(flags.NewClientOptions()) 164 assert.NilError(t, err) 165 assert.DeepEqual(t, cli.ServerInfo(), testcase.expectedServer) 166 assert.Equal(t, apiclient.negotiated, testcase.negotiated) 167 }) 168 } 169 } 170 171 // Makes sure we don't hang forever on the initial connection. 172 // https://github.com/docker/cli/issues/3652 173 func TestInitializeFromClientHangs(t *testing.T) { 174 dir := t.TempDir() 175 socket := filepath.Join(dir, "my.sock") 176 l, err := net.Listen("unix", socket) 177 assert.NilError(t, err) 178 179 receiveReqCh := make(chan bool) 180 timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Second) 181 defer cancel() 182 183 // Simulate a server that hangs on connections. 184 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 select { 186 case <-timeoutCtx.Done(): 187 case receiveReqCh <- true: // Blocks until someone receives on the channel. 188 } 189 _, _ = w.Write([]byte("OK")) 190 })) 191 ts.Listener = l 192 ts.Start() 193 defer ts.Close() 194 195 opts := &flags.ClientOptions{Hosts: []string{fmt.Sprintf("unix://%s", socket)}} 196 configFile := &configfile.ConfigFile{} 197 apiClient, err := NewAPIClientFromFlags(opts, configFile) 198 assert.NilError(t, err) 199 200 initializedCh := make(chan bool) 201 202 go func() { 203 cli := &DockerCli{client: apiClient, initTimeout: time.Millisecond} 204 err := cli.Initialize(flags.NewClientOptions()) 205 assert.Check(t, err) 206 cli.CurrentVersion() 207 close(initializedCh) 208 }() 209 210 select { 211 case <-timeoutCtx.Done(): 212 t.Fatal("timeout waiting for initialization to complete") 213 case <-initializedCh: 214 } 215 216 select { 217 case <-timeoutCtx.Done(): 218 t.Fatal("server never received an init request") 219 case <-receiveReqCh: 220 } 221 } 222 223 // The CLI no longer disables/hides experimental CLI features, however, we need 224 // to verify that existing configuration files do not break 225 func TestExperimentalCLI(t *testing.T) { 226 defaultVersion := "v1.55" 227 228 testcases := []struct { 229 doc string 230 configfile string 231 }{ 232 { 233 doc: "default", 234 configfile: `{}`, 235 }, 236 { 237 doc: "experimental", 238 configfile: `{ 239 "experimental": "enabled" 240 }`, 241 }, 242 } 243 244 for _, testcase := range testcases { 245 testcase := testcase 246 t.Run(testcase.doc, func(t *testing.T) { 247 dir := fs.NewDir(t, testcase.doc, fs.WithFile("config.json", testcase.configfile)) 248 defer dir.Remove() 249 apiclient := &fakeClient{ 250 version: defaultVersion, 251 pingFunc: func() (types.Ping, error) { 252 return types.Ping{Experimental: true, OSType: "linux", APIVersion: defaultVersion}, nil 253 }, 254 } 255 256 cli := &DockerCli{client: apiclient, err: os.Stderr} 257 config.SetDir(dir.Path()) 258 err := cli.Initialize(flags.NewClientOptions()) 259 assert.NilError(t, err) 260 }) 261 } 262 } 263 264 func TestNewDockerCliAndOperators(t *testing.T) { 265 // Test default operations and also overriding default ones 266 cli, err := NewDockerCli( 267 WithContentTrust(true), 268 ) 269 assert.NilError(t, err) 270 // Check streams are initialized 271 assert.Check(t, cli.In() != nil) 272 assert.Check(t, cli.Out() != nil) 273 assert.Check(t, cli.Err() != nil) 274 assert.Equal(t, cli.ContentTrustEnabled(), true) 275 276 // Apply can modify a dockerCli after construction 277 inbuf := bytes.NewBuffer([]byte("input")) 278 outbuf := bytes.NewBuffer(nil) 279 errbuf := bytes.NewBuffer(nil) 280 err = cli.Apply( 281 WithInputStream(io.NopCloser(inbuf)), 282 WithOutputStream(outbuf), 283 WithErrorStream(errbuf), 284 ) 285 assert.NilError(t, err) 286 // Check input stream 287 inputStream, err := io.ReadAll(cli.In()) 288 assert.NilError(t, err) 289 assert.Equal(t, string(inputStream), "input") 290 // Check output stream 291 fmt.Fprintf(cli.Out(), "output") 292 outputStream, err := io.ReadAll(outbuf) 293 assert.NilError(t, err) 294 assert.Equal(t, string(outputStream), "output") 295 // Check error stream 296 fmt.Fprintf(cli.Err(), "error") 297 errStream, err := io.ReadAll(errbuf) 298 assert.NilError(t, err) 299 assert.Equal(t, string(errStream), "error") 300 } 301 302 func TestInitializeShouldAlwaysCreateTheContextStore(t *testing.T) { 303 cli, err := NewDockerCli() 304 assert.NilError(t, err) 305 assert.NilError(t, cli.Initialize(flags.NewClientOptions(), WithInitializeClient(func(cli *DockerCli) (client.APIClient, error) { 306 return client.NewClientWithOpts() 307 }))) 308 assert.Check(t, cli.ContextStore() != nil) 309 }