github.com/anonymouse64/snapd@v0.0.0-20210824153203-04c4c42d842d/client/client_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package client_test
    21  
    22  import (
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"net"
    29  	"net/http"
    30  	"net/http/httptest"
    31  	"net/url"
    32  	"os"
    33  	"path/filepath"
    34  	"strings"
    35  	"testing"
    36  	"time"
    37  
    38  	. "gopkg.in/check.v1"
    39  
    40  	"github.com/snapcore/snapd/client"
    41  	"github.com/snapcore/snapd/dirs"
    42  	"github.com/snapcore/snapd/testutil"
    43  )
    44  
    45  // Hook up check.v1 into the "go test" runner
    46  func Test(t *testing.T) { TestingT(t) }
    47  
    48  type clientSuite struct {
    49  	testutil.BaseTest
    50  
    51  	cli           *client.Client
    52  	req           *http.Request
    53  	reqs          []*http.Request
    54  	rsp           string
    55  	rsps          []string
    56  	err           error
    57  	doCalls       int
    58  	header        http.Header
    59  	status        int
    60  	contentLength int64
    61  
    62  	countingCloser *countingCloser
    63  }
    64  
    65  var _ = Suite(&clientSuite{})
    66  
    67  func (cs *clientSuite) SetUpTest(c *C) {
    68  	os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "auth.json"))
    69  	cs.AddCleanup(func() { os.Unsetenv(client.TestAuthFileEnvKey) })
    70  
    71  	cs.cli = client.New(nil)
    72  	cs.cli.SetDoer(cs)
    73  	cs.err = nil
    74  	cs.req = nil
    75  	cs.reqs = nil
    76  	cs.rsp = ""
    77  	cs.rsps = nil
    78  	cs.req = nil
    79  	cs.header = nil
    80  	cs.status = 200
    81  	cs.doCalls = 0
    82  	cs.contentLength = 0
    83  	cs.countingCloser = nil
    84  
    85  	dirs.SetRootDir(c.MkDir())
    86  	cs.AddCleanup(func() { dirs.SetRootDir("") })
    87  
    88  	cs.AddCleanup(client.MockDoTimings(time.Millisecond, 100*time.Millisecond))
    89  }
    90  
    91  type countingCloser struct {
    92  	io.Reader
    93  	closeCalled int
    94  }
    95  
    96  func (n *countingCloser) Close() error {
    97  	n.closeCalled++
    98  	return nil
    99  }
   100  
   101  func (cs *clientSuite) Do(req *http.Request) (*http.Response, error) {
   102  	cs.req = req
   103  	cs.reqs = append(cs.reqs, req)
   104  	body := cs.rsp
   105  	if cs.doCalls < len(cs.rsps) {
   106  		body = cs.rsps[cs.doCalls]
   107  	}
   108  	cs.countingCloser = &countingCloser{Reader: strings.NewReader(body)}
   109  	rsp := &http.Response{
   110  		Body:          cs.countingCloser,
   111  		Header:        cs.header,
   112  		StatusCode:    cs.status,
   113  		ContentLength: cs.contentLength,
   114  	}
   115  	cs.doCalls++
   116  	return rsp, cs.err
   117  }
   118  
   119  func (cs *clientSuite) TestNewPanics(c *C) {
   120  	c.Assert(func() {
   121  		client.New(&client.Config{BaseURL: ":"})
   122  	}, PanicMatches, `cannot parse server base URL: ":" \(parse \"?:\"?: missing protocol scheme\)`)
   123  }
   124  
   125  func (cs *clientSuite) TestClientDoReportsErrors(c *C) {
   126  	cs.err = errors.New("ouchie")
   127  	_, err := cs.cli.Do("GET", "/", nil, nil, nil, nil)
   128  	c.Check(err, ErrorMatches, "cannot communicate with server: ouchie")
   129  	if cs.doCalls < 2 {
   130  		c.Fatalf("do did not retry")
   131  	}
   132  }
   133  
   134  func (cs *clientSuite) TestClientWorks(c *C) {
   135  	var v []int
   136  	cs.rsp = `[1,2]`
   137  	reqBody := ioutil.NopCloser(strings.NewReader(""))
   138  	statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil)
   139  	c.Check(err, IsNil)
   140  	c.Check(statusCode, Equals, 200)
   141  	c.Check(v, DeepEquals, []int{1, 2})
   142  	c.Assert(cs.req, NotNil)
   143  	c.Assert(cs.req.URL, NotNil)
   144  	c.Check(cs.req.Method, Equals, "GET")
   145  	c.Check(cs.req.Body, Equals, reqBody)
   146  	c.Check(cs.req.URL.Path, Equals, "/this")
   147  }
   148  
   149  func makeMaintenanceFile(c *C, b []byte) {
   150  	c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdMaintenanceFile), 0755), IsNil)
   151  	c.Assert(ioutil.WriteFile(dirs.SnapdMaintenanceFile, b, 0644), IsNil)
   152  }
   153  
   154  func (cs *clientSuite) TestClientSetMaintenanceForMaintenanceJSON(c *C) {
   155  	// write a maintenance.json that says snapd is down for a restart
   156  	maintErr := &client.Error{
   157  		Kind:    client.ErrorKindSystemRestart,
   158  		Message: "system is restarting",
   159  	}
   160  	b, err := json.Marshal(maintErr)
   161  	c.Assert(err, IsNil)
   162  	makeMaintenanceFile(c, b)
   163  
   164  	// now after a Do(), we will have maintenance set to what we wrote
   165  	// originally
   166  	_, err = cs.cli.Do("GET", "/this", nil, nil, nil, nil)
   167  	c.Check(err, IsNil)
   168  
   169  	returnedErr := cs.cli.Maintenance()
   170  	c.Assert(returnedErr, DeepEquals, maintErr)
   171  }
   172  
   173  func (cs *clientSuite) TestClientIgnoresGarbageMaintenanceJSON(c *C) {
   174  	// write a garbage maintenance.json that can't be unmarshalled
   175  	makeMaintenanceFile(c, []byte("blah blah blah not json"))
   176  
   177  	// after a Do(), no maintenance set and also no error returned from Do()
   178  	_, err := cs.cli.Do("GET", "/this", nil, nil, nil, nil)
   179  	c.Check(err, IsNil)
   180  
   181  	returnedErr := cs.cli.Maintenance()
   182  	c.Assert(returnedErr, IsNil)
   183  }
   184  
   185  func (cs *clientSuite) TestClientDoNoTimeoutIgnoresRetry(c *C) {
   186  	var v []int
   187  	cs.rsp = `[1,2]`
   188  	cs.err = fmt.Errorf("borken")
   189  	reqBody := ioutil.NopCloser(strings.NewReader(""))
   190  	doOpts := &client.DoOptions{
   191  		// Timeout is unset, thus 0, and thus we ignore the retry and only run
   192  		// once even though there is an error
   193  		Retry: time.Duration(time.Second),
   194  	}
   195  	_, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, doOpts)
   196  	c.Check(err, ErrorMatches, "cannot communicate with server: borken")
   197  	c.Assert(cs.doCalls, Equals, 1)
   198  }
   199  
   200  func (cs *clientSuite) TestClientDoRetryValidation(c *C) {
   201  	var v []int
   202  	cs.rsp = `[1,2]`
   203  	reqBody := ioutil.NopCloser(strings.NewReader(""))
   204  	doOpts := &client.DoOptions{
   205  		Retry:   time.Duration(-1),
   206  		Timeout: time.Duration(time.Minute),
   207  	}
   208  	_, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, doOpts)
   209  	c.Check(err, ErrorMatches, "internal error: retry setting.*invalid")
   210  	c.Assert(cs.req, IsNil)
   211  }
   212  
   213  func (cs *clientSuite) TestClientDoRetryWorks(c *C) {
   214  	reqBody := ioutil.NopCloser(strings.NewReader(""))
   215  	cs.err = fmt.Errorf("borken")
   216  	doOpts := &client.DoOptions{
   217  		Retry:   time.Duration(time.Millisecond),
   218  		Timeout: time.Duration(time.Second),
   219  	}
   220  	_, err := cs.cli.Do("GET", "/this", nil, reqBody, nil, doOpts)
   221  	c.Check(err, ErrorMatches, "cannot communicate with server: borken")
   222  	// best effort checking given that execution could be slow
   223  	// on some machines
   224  	c.Assert(cs.doCalls > 100, Equals, true, Commentf("got only %v calls", cs.doCalls))
   225  	c.Assert(cs.doCalls < 1100, Equals, true, Commentf("got %v calls", cs.doCalls))
   226  }
   227  
   228  func (cs *clientSuite) TestClientUnderstandsStatusCode(c *C) {
   229  	var v []int
   230  	cs.status = 202
   231  	cs.rsp = `[1,2]`
   232  	reqBody := ioutil.NopCloser(strings.NewReader(""))
   233  	statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v, nil)
   234  	c.Check(err, IsNil)
   235  	c.Check(statusCode, Equals, 202)
   236  	c.Check(v, DeepEquals, []int{1, 2})
   237  	c.Assert(cs.req, NotNil)
   238  	c.Assert(cs.req.URL, NotNil)
   239  	c.Check(cs.req.Method, Equals, "GET")
   240  	c.Check(cs.req.Body, Equals, reqBody)
   241  	c.Check(cs.req.URL.Path, Equals, "/this")
   242  }
   243  
   244  func (cs *clientSuite) TestClientDefaultsToNoAuthorization(c *C) {
   245  	os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json"))
   246  	defer os.Unsetenv(client.TestAuthFileEnvKey)
   247  
   248  	var v string
   249  	_, _ = cs.cli.Do("GET", "/this", nil, nil, &v, nil)
   250  	c.Assert(cs.req, NotNil)
   251  	authorization := cs.req.Header.Get("Authorization")
   252  	c.Check(authorization, Equals, "")
   253  }
   254  
   255  func (cs *clientSuite) TestClientSetsAuthorization(c *C) {
   256  	os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json"))
   257  	defer os.Unsetenv(client.TestAuthFileEnvKey)
   258  
   259  	mockUserData := client.User{
   260  		Macaroon:   "macaroon",
   261  		Discharges: []string{"discharge"},
   262  	}
   263  	err := client.TestWriteAuth(mockUserData)
   264  	c.Assert(err, IsNil)
   265  
   266  	var v string
   267  	_, _ = cs.cli.Do("GET", "/this", nil, nil, &v, nil)
   268  	authorization := cs.req.Header.Get("Authorization")
   269  	c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`)
   270  }
   271  
   272  func (cs *clientSuite) TestClientHonorsDisableAuth(c *C) {
   273  	os.Setenv(client.TestAuthFileEnvKey, filepath.Join(c.MkDir(), "json"))
   274  	defer os.Unsetenv(client.TestAuthFileEnvKey)
   275  
   276  	mockUserData := client.User{
   277  		Macaroon:   "macaroon",
   278  		Discharges: []string{"discharge"},
   279  	}
   280  	err := client.TestWriteAuth(mockUserData)
   281  	c.Assert(err, IsNil)
   282  
   283  	var v string
   284  	cli := client.New(&client.Config{DisableAuth: true})
   285  	cli.SetDoer(cs)
   286  	_, _ = cli.Do("GET", "/this", nil, nil, &v, nil)
   287  	authorization := cs.req.Header.Get("Authorization")
   288  	c.Check(authorization, Equals, "")
   289  }
   290  
   291  func (cs *clientSuite) TestClientHonorsInteractive(c *C) {
   292  	var v string
   293  	cli := client.New(&client.Config{Interactive: false})
   294  	cli.SetDoer(cs)
   295  	_, _ = cli.Do("GET", "/this", nil, nil, &v, nil)
   296  	interactive := cs.req.Header.Get(client.AllowInteractionHeader)
   297  	c.Check(interactive, Equals, "")
   298  
   299  	cli = client.New(&client.Config{Interactive: true})
   300  	cli.SetDoer(cs)
   301  	_, _ = cli.Do("GET", "/this", nil, nil, &v, nil)
   302  	interactive = cs.req.Header.Get(client.AllowInteractionHeader)
   303  	c.Check(interactive, Equals, "true")
   304  }
   305  
   306  func (cs *clientSuite) TestClientWhoAmINobody(c *C) {
   307  	email, err := cs.cli.WhoAmI()
   308  	c.Assert(err, IsNil)
   309  	c.Check(email, Equals, "")
   310  }
   311  
   312  func (cs *clientSuite) TestClientWhoAmIRubbish(c *C) {
   313  	c.Assert(ioutil.WriteFile(client.TestStoreAuthFilename(os.Getenv("HOME")), []byte("rubbish"), 0644), IsNil)
   314  
   315  	email, err := cs.cli.WhoAmI()
   316  	c.Check(err, NotNil)
   317  	c.Check(email, Equals, "")
   318  }
   319  
   320  func (cs *clientSuite) TestClientWhoAmISomebody(c *C) {
   321  	mockUserData := client.User{
   322  		Email: "foo@example.com",
   323  	}
   324  	c.Assert(client.TestWriteAuth(mockUserData), IsNil)
   325  
   326  	email, err := cs.cli.WhoAmI()
   327  	c.Check(err, IsNil)
   328  	c.Check(email, Equals, "foo@example.com")
   329  }
   330  
   331  func (cs *clientSuite) TestClientSysInfo(c *C) {
   332  	cs.rsp = `{"type": "sync", "result":
   333                       {"series": "16",
   334                        "version": "2",
   335                        "os-release": {"id": "ubuntu", "version-id": "16.04"},
   336                        "on-classic": true,
   337                        "build-id": "1234",
   338                        "confinement": "strict",
   339                        "architecture": "TI-99/4A",
   340                        "virtualization": "MESS",
   341                        "sandbox-features": {"backend": ["feature-1", "feature-2"]}}}`
   342  	sysInfo, err := cs.cli.SysInfo()
   343  	c.Check(err, IsNil)
   344  	c.Check(sysInfo, DeepEquals, &client.SysInfo{
   345  		Version: "2",
   346  		Series:  "16",
   347  		OSRelease: client.OSRelease{
   348  			ID:        "ubuntu",
   349  			VersionID: "16.04",
   350  		},
   351  		OnClassic:   true,
   352  		Confinement: "strict",
   353  		SandboxFeatures: map[string][]string{
   354  			"backend": {"feature-1", "feature-2"},
   355  		},
   356  		BuildID:        "1234",
   357  		Architecture:   "TI-99/4A",
   358  		Virtualization: "MESS",
   359  	})
   360  }
   361  
   362  func (cs *clientSuite) TestServerVersion(c *C) {
   363  	cs.rsp = `{"type": "sync", "result":
   364                       {"series": "16",
   365                        "version": "2",
   366                        "os-release": {"id": "zyggy", "version-id": "123"},
   367                        "architecture": "m32",
   368                        "virtualization": "qemu"
   369  }}}`
   370  	version, err := cs.cli.ServerVersion()
   371  	c.Check(err, IsNil)
   372  	c.Check(version, DeepEquals, &client.ServerVersion{
   373  		Version:        "2",
   374  		Series:         "16",
   375  		OSID:           "zyggy",
   376  		OSVersionID:    "123",
   377  		Architecture:   "m32",
   378  		Virtualization: "qemu",
   379  	})
   380  }
   381  
   382  func (cs *clientSuite) TestSnapdClientIntegration(c *C) {
   383  	c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapdSocket), 0755), IsNil)
   384  	l, err := net.Listen("unix", dirs.SnapdSocket)
   385  	if err != nil {
   386  		c.Fatalf("unable to listen on %q: %v", dirs.SnapdSocket, err)
   387  	}
   388  
   389  	f := func(w http.ResponseWriter, r *http.Request) {
   390  		c.Check(r.URL.Path, Equals, "/v2/system-info")
   391  		c.Check(r.URL.RawQuery, Equals, "")
   392  
   393  		fmt.Fprintln(w, `{"type":"sync", "result":{"series":"42"}}`)
   394  	}
   395  
   396  	srv := &httptest.Server{
   397  		Listener: l,
   398  		Config:   &http.Server{Handler: http.HandlerFunc(f)},
   399  	}
   400  	srv.Start()
   401  	defer srv.Close()
   402  
   403  	cli := client.New(nil)
   404  	si, err := cli.SysInfo()
   405  	c.Assert(err, IsNil)
   406  	c.Check(si.Series, Equals, "42")
   407  }
   408  
   409  func (cs *clientSuite) TestSnapClientIntegration(c *C) {
   410  	c.Assert(os.MkdirAll(filepath.Dir(dirs.SnapSocket), 0755), IsNil)
   411  	l, err := net.Listen("unix", dirs.SnapSocket)
   412  	if err != nil {
   413  		c.Fatalf("unable to listen on %q: %v", dirs.SnapSocket, err)
   414  	}
   415  
   416  	f := func(w http.ResponseWriter, r *http.Request) {
   417  		c.Check(r.URL.Path, Equals, "/v2/snapctl")
   418  		c.Check(r.URL.RawQuery, Equals, "")
   419  
   420  		fmt.Fprintln(w, `{"type":"sync", "result":{"stdout":"test stdout","stderr":"test stderr"}}`)
   421  	}
   422  
   423  	srv := &httptest.Server{
   424  		Listener: l,
   425  		Config:   &http.Server{Handler: http.HandlerFunc(f)},
   426  	}
   427  	srv.Start()
   428  	defer srv.Close()
   429  
   430  	cli := client.New(&client.Config{
   431  		Socket: dirs.SnapSocket,
   432  	})
   433  	options := &client.SnapCtlOptions{
   434  		ContextID: "foo",
   435  		Args:      []string{"bar", "--baz"},
   436  	}
   437  
   438  	stdout, stderr, err := cli.RunSnapctl(options, nil)
   439  	c.Check(err, IsNil)
   440  	c.Check(string(stdout), Equals, "test stdout")
   441  	c.Check(string(stderr), Equals, "test stderr")
   442  }
   443  
   444  func (cs *clientSuite) TestClientReportsOpError(c *C) {
   445  	cs.status = 500
   446  	cs.rsp = `{"type": "error"}`
   447  	_, err := cs.cli.SysInfo()
   448  	c.Check(err, ErrorMatches, `.*server error: "Internal Server Error"`)
   449  }
   450  
   451  func (cs *clientSuite) TestClientReportsOpErrorStr(c *C) {
   452  	cs.status = 400
   453  	cs.rsp = `{
   454  		"result": {},
   455  		"status": "Bad Request",
   456  		"status-code": 400,
   457  		"type": "error"
   458  	}`
   459  	_, err := cs.cli.SysInfo()
   460  	c.Check(err, ErrorMatches, `.*server error: "Bad Request"`)
   461  }
   462  
   463  func (cs *clientSuite) TestClientReportsBadType(c *C) {
   464  	cs.rsp = `{"type": "what"}`
   465  	_, err := cs.cli.SysInfo()
   466  	c.Check(err, ErrorMatches, `.*expected sync response, got "what"`)
   467  }
   468  
   469  func (cs *clientSuite) TestClientReportsOuterJSONError(c *C) {
   470  	cs.rsp = "this isn't really json is it"
   471  	_, err := cs.cli.SysInfo()
   472  	c.Check(err, ErrorMatches, `.*invalid character .*`)
   473  }
   474  
   475  func (cs *clientSuite) TestClientReportsInnerJSONError(c *C) {
   476  	cs.rsp = `{"type": "sync", "result": "this isn't really json is it"}`
   477  	_, err := cs.cli.SysInfo()
   478  	c.Check(err, ErrorMatches, `.*cannot unmarshal.*`)
   479  }
   480  
   481  func (cs *clientSuite) TestClientMaintenance(c *C) {
   482  	cs.rsp = `{"type":"sync", "result":{"series":"42"}, "maintenance": {"kind": "system-restart", "message": "system is restarting"}}`
   483  	_, err := cs.cli.SysInfo()
   484  	c.Assert(err, IsNil)
   485  	c.Check(cs.cli.Maintenance().(*client.Error), DeepEquals, &client.Error{
   486  		Kind:    client.ErrorKindSystemRestart,
   487  		Message: "system is restarting",
   488  	})
   489  
   490  	cs.rsp = `{"type":"sync", "result":{"series":"42"}}`
   491  	_, err = cs.cli.SysInfo()
   492  	c.Assert(err, IsNil)
   493  	c.Check(cs.cli.Maintenance(), Equals, error(nil))
   494  }
   495  
   496  func (cs *clientSuite) TestClientAsyncOpMaintenance(c *C) {
   497  	cs.status = 202
   498  	cs.rsp = `{"type":"async", "status-code": 202, "change": "42", "maintenance": {"kind": "system-restart", "message": "system is restarting"}}`
   499  	_, err := cs.cli.Install("foo", nil)
   500  	c.Assert(err, IsNil)
   501  	c.Check(cs.cli.Maintenance().(*client.Error), DeepEquals, &client.Error{
   502  		Kind:    client.ErrorKindSystemRestart,
   503  		Message: "system is restarting",
   504  	})
   505  
   506  	cs.rsp = `{"type":"async", "status-code": 202, "change": "42"}`
   507  	_, err = cs.cli.Install("foo", nil)
   508  	c.Assert(err, IsNil)
   509  	c.Check(cs.cli.Maintenance(), Equals, error(nil))
   510  }
   511  
   512  func (cs *clientSuite) TestParseError(c *C) {
   513  	resp := &http.Response{
   514  		Status: "404 Not Found",
   515  	}
   516  	err := client.ParseErrorInTest(resp)
   517  	c.Check(err, ErrorMatches, `server error: "404 Not Found"`)
   518  
   519  	h := http.Header{}
   520  	h.Add("Content-Type", "application/json")
   521  	resp = &http.Response{
   522  		Status: "400 Bad Request",
   523  		Header: h,
   524  		Body: ioutil.NopCloser(strings.NewReader(`{
   525  			"status-code": 400,
   526  			"type": "error",
   527  			"result": {
   528  				"message": "invalid"
   529  			}
   530  		}`)),
   531  	}
   532  	err = client.ParseErrorInTest(resp)
   533  	c.Check(err, ErrorMatches, "invalid")
   534  
   535  	resp = &http.Response{
   536  		Status: "400 Bad Request",
   537  		Header: h,
   538  		Body:   ioutil.NopCloser(strings.NewReader("{}")),
   539  	}
   540  	err = client.ParseErrorInTest(resp)
   541  	c.Check(err, ErrorMatches, `server error: "400 Bad Request"`)
   542  }
   543  
   544  func (cs *clientSuite) TestIsTwoFactor(c *C) {
   545  	c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorRequired}), Equals, true)
   546  	c.Check(client.IsTwoFactorError(&client.Error{Kind: client.ErrorKindTwoFactorFailed}), Equals, true)
   547  	c.Check(client.IsTwoFactorError(&client.Error{Kind: "some other kind"}), Equals, false)
   548  	c.Check(client.IsTwoFactorError(errors.New("test")), Equals, false)
   549  	c.Check(client.IsTwoFactorError(nil), Equals, false)
   550  	c.Check(client.IsTwoFactorError((*client.Error)(nil)), Equals, false)
   551  }
   552  
   553  func (cs *clientSuite) TestIsRetryable(c *C) {
   554  	// unhappy
   555  	c.Check(client.IsRetryable(nil), Equals, false)
   556  	c.Check(client.IsRetryable(errors.New("some-error")), Equals, false)
   557  	c.Check(client.IsRetryable(&client.Error{Kind: "something-else"}), Equals, false)
   558  	// happy
   559  	c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindSnapChangeConflict}), Equals, true)
   560  }
   561  
   562  func (cs *clientSuite) TestUserAgent(c *C) {
   563  	cli := client.New(&client.Config{UserAgent: "some-agent/9.87"})
   564  	cli.SetDoer(cs)
   565  
   566  	var v string
   567  	_, _ = cli.Do("GET", "/", nil, nil, &v, nil)
   568  	c.Assert(cs.req, NotNil)
   569  	c.Check(cs.req.Header.Get("User-Agent"), Equals, "some-agent/9.87")
   570  }
   571  
   572  func (cs *clientSuite) TestDebugEnsureStateSoon(c *C) {
   573  	cs.rsp = `{"type": "sync", "result":true}`
   574  	err := cs.cli.Debug("ensure-state-soon", nil, nil)
   575  	c.Check(err, IsNil)
   576  	c.Check(cs.reqs, HasLen, 1)
   577  	c.Check(cs.reqs[0].Method, Equals, "POST")
   578  	c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug")
   579  	data, err := ioutil.ReadAll(cs.reqs[0].Body)
   580  	c.Assert(err, IsNil)
   581  	c.Check(data, DeepEquals, []byte(`{"action":"ensure-state-soon"}`))
   582  }
   583  
   584  func (cs *clientSuite) TestDebugGeneric(c *C) {
   585  	cs.rsp = `{"type": "sync", "result":["res1","res2"]}`
   586  
   587  	var result []string
   588  	err := cs.cli.Debug("do-something", []string{"param1", "param2"}, &result)
   589  	c.Check(err, IsNil)
   590  	c.Check(result, DeepEquals, []string{"res1", "res2"})
   591  	c.Check(cs.reqs, HasLen, 1)
   592  	c.Check(cs.reqs[0].Method, Equals, "POST")
   593  	c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug")
   594  	data, err := ioutil.ReadAll(cs.reqs[0].Body)
   595  	c.Assert(err, IsNil)
   596  	c.Check(string(data), DeepEquals, `{"action":"do-something","params":["param1","param2"]}`)
   597  }
   598  
   599  func (cs *clientSuite) TestDebugGet(c *C) {
   600  	cs.rsp = `{"type": "sync", "result":["res1","res2"]}`
   601  
   602  	var result []string
   603  	err := cs.cli.DebugGet("do-something", &result, map[string]string{"foo": "bar"})
   604  	c.Check(err, IsNil)
   605  	c.Check(result, DeepEquals, []string{"res1", "res2"})
   606  	c.Check(cs.reqs, HasLen, 1)
   607  	c.Check(cs.reqs[0].Method, Equals, "GET")
   608  	c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug")
   609  	c.Check(cs.reqs[0].URL.Query(), DeepEquals, url.Values{"aspect": []string{"do-something"}, "foo": []string{"bar"}})
   610  }
   611  
   612  type integrationSuite struct{}
   613  
   614  var _ = Suite(&integrationSuite{})
   615  
   616  func (cs *integrationSuite) TestClientTimeoutLP1837804(c *C) {
   617  	restore := client.MockDoTimings(time.Millisecond, 5*time.Millisecond)
   618  	defer restore()
   619  
   620  	testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {
   621  		time.Sleep(25 * time.Millisecond)
   622  	}))
   623  	defer func() { testServer.Close() }()
   624  
   625  	cli := client.New(&client.Config{BaseURL: testServer.URL})
   626  	_, err := cli.Do("GET", "/", nil, nil, nil, nil)
   627  	c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`)
   628  
   629  	_, err = cli.Do("POST", "/", nil, nil, nil, nil)
   630  	c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`)
   631  }
   632  
   633  func (cs *clientSuite) TestClientSystemRecoveryKeys(c *C) {
   634  	cs.rsp = `{"type":"sync", "result":{"recovery-key":"42"}}`
   635  
   636  	var key client.SystemRecoveryKeysResponse
   637  	err := cs.cli.SystemRecoveryKeys(&key)
   638  	c.Assert(err, IsNil)
   639  	c.Check(cs.reqs, HasLen, 1)
   640  	c.Check(cs.reqs[0].Method, Equals, "GET")
   641  	c.Check(cs.reqs[0].URL.Path, Equals, "/v2/system-recovery-keys")
   642  	c.Check(key.RecoveryKey, Equals, "42")
   643  }