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