github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/client/snap_op_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 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 "mime" 29 "mime/multipart" 30 "net/http" 31 "path/filepath" 32 33 "gopkg.in/check.v1" 34 35 "github.com/snapcore/snapd/client" 36 ) 37 38 var chanName = "achan" 39 40 var ops = []struct { 41 op func(*client.Client, string, *client.SnapOptions) (string, error) 42 action string 43 }{ 44 {(*client.Client).Install, "install"}, 45 {(*client.Client).Refresh, "refresh"}, 46 {(*client.Client).Remove, "remove"}, 47 {(*client.Client).Revert, "revert"}, 48 {(*client.Client).Enable, "enable"}, 49 {(*client.Client).Disable, "disable"}, 50 {(*client.Client).Switch, "switch"}, 51 } 52 53 var multiOps = []struct { 54 op func(*client.Client, []string, *client.SnapOptions) (string, error) 55 action string 56 }{ 57 {(*client.Client).RefreshMany, "refresh"}, 58 {(*client.Client).InstallMany, "install"}, 59 {(*client.Client).RemoveMany, "remove"}, 60 } 61 62 func (cs *clientSuite) TestClientOpSnapServerError(c *check.C) { 63 cs.err = errors.New("fail") 64 for _, s := range ops { 65 _, err := s.op(cs.cli, pkgName, nil) 66 c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) 67 } 68 } 69 70 func (cs *clientSuite) TestClientMultiOpSnapServerError(c *check.C) { 71 cs.err = errors.New("fail") 72 for _, s := range multiOps { 73 _, err := s.op(cs.cli, nil, nil) 74 c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) 75 } 76 _, _, err := cs.cli.SnapshotMany(nil, nil) 77 c.Check(err, check.ErrorMatches, `.*fail`) 78 } 79 80 func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) { 81 cs.status = 400 82 cs.rsp = `{"type": "error"}` 83 for _, s := range ops { 84 _, err := s.op(cs.cli, pkgName, nil) 85 c.Check(err, check.ErrorMatches, `.*server error: "Bad Request"`, check.Commentf(s.action)) 86 } 87 } 88 89 func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) { 90 cs.status = 500 91 cs.rsp = `{"type": "error"}` 92 for _, s := range multiOps { 93 _, err := s.op(cs.cli, nil, nil) 94 c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`, check.Commentf(s.action)) 95 } 96 _, _, err := cs.cli.SnapshotMany(nil, nil) 97 c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`) 98 } 99 100 func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) { 101 cs.rsp = `{"type": "what"}` 102 for _, s := range ops { 103 _, err := s.op(cs.cli, pkgName, nil) 104 c.Check(err, check.ErrorMatches, `.*expected async response for "POST" on "/v2/snaps/`+pkgName+`", got "what"`, check.Commentf(s.action)) 105 } 106 } 107 108 func (cs *clientSuite) TestClientOpSnapNotAccepted(c *check.C) { 109 cs.rsp = `{ 110 "status-code": 200, 111 "type": "async" 112 }` 113 for _, s := range ops { 114 _, err := s.op(cs.cli, pkgName, nil) 115 c.Check(err, check.ErrorMatches, `.*operation not accepted`, check.Commentf(s.action)) 116 } 117 } 118 119 func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) { 120 cs.status = 202 121 cs.rsp = `{ 122 "status-code": 202, 123 "type": "async" 124 }` 125 for _, s := range ops { 126 _, err := s.op(cs.cli, pkgName, nil) 127 c.Assert(err, check.ErrorMatches, `.*response without change reference.*`, check.Commentf(s.action)) 128 } 129 } 130 131 func (cs *clientSuite) TestClientOpSnap(c *check.C) { 132 cs.status = 202 133 cs.rsp = `{ 134 "change": "d728", 135 "status-code": 202, 136 "type": "async" 137 }` 138 for _, s := range ops { 139 id, err := s.op(cs.cli, pkgName, &client.SnapOptions{Channel: chanName}) 140 c.Assert(err, check.IsNil) 141 142 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) 143 144 _, ok := cs.req.Context().Deadline() 145 c.Check(ok, check.Equals, true) 146 147 body, err := ioutil.ReadAll(cs.req.Body) 148 c.Assert(err, check.IsNil, check.Commentf(s.action)) 149 jsonBody := make(map[string]string) 150 err = json.Unmarshal(body, &jsonBody) 151 c.Assert(err, check.IsNil, check.Commentf(s.action)) 152 c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) 153 c.Check(jsonBody["channel"], check.Equals, chanName, check.Commentf(s.action)) 154 c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) 155 156 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName), check.Commentf(s.action)) 157 c.Check(id, check.Equals, "d728", check.Commentf(s.action)) 158 } 159 } 160 161 func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) { 162 cs.status = 202 163 cs.rsp = `{ 164 "change": "d728", 165 "status-code": 202, 166 "type": "async" 167 }` 168 for _, s := range multiOps { 169 // Note body is essentially the same as TestClientMultiSnapshot; keep in sync 170 id, err := s.op(cs.cli, []string{pkgName}, nil) 171 c.Assert(err, check.IsNil) 172 173 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) 174 175 body, err := ioutil.ReadAll(cs.req.Body) 176 c.Assert(err, check.IsNil, check.Commentf(s.action)) 177 jsonBody := make(map[string]interface{}) 178 err = json.Unmarshal(body, &jsonBody) 179 c.Assert(err, check.IsNil, check.Commentf(s.action)) 180 c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) 181 c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}, check.Commentf(s.action)) 182 c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) 183 184 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", check.Commentf(s.action)) 185 c.Check(id, check.Equals, "d728", check.Commentf(s.action)) 186 } 187 } 188 189 func (cs *clientSuite) TestClientMultiSnapshot(c *check.C) { 190 // Note body is essentially the same as TestClientMultiOpSnap; keep in sync 191 cs.status = 202 192 cs.rsp = `{ 193 "result": {"set-id": 42}, 194 "change": "d728", 195 "status-code": 202, 196 "type": "async" 197 }` 198 setID, changeID, err := cs.cli.SnapshotMany([]string{pkgName}, nil) 199 c.Assert(err, check.IsNil) 200 c.Check(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") 201 202 body, err := ioutil.ReadAll(cs.req.Body) 203 c.Assert(err, check.IsNil) 204 jsonBody := make(map[string]interface{}) 205 err = json.Unmarshal(body, &jsonBody) 206 c.Assert(err, check.IsNil) 207 c.Check(jsonBody["action"], check.Equals, "snapshot") 208 c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}) 209 c.Check(jsonBody, check.HasLen, 2) 210 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") 211 c.Check(setID, check.Equals, uint64(42)) 212 c.Check(changeID, check.Equals, "d728") 213 } 214 215 func (cs *clientSuite) TestClientOpInstallPath(c *check.C) { 216 cs.status = 202 217 cs.rsp = `{ 218 "change": "66b3", 219 "status-code": 202, 220 "type": "async" 221 }` 222 bodyData := []byte("snap-data") 223 224 snap := filepath.Join(c.MkDir(), "foo.snap") 225 err := ioutil.WriteFile(snap, bodyData, 0644) 226 c.Assert(err, check.IsNil) 227 228 id, err := cs.cli.InstallPath(snap, "", nil) 229 c.Assert(err, check.IsNil) 230 231 body, err := ioutil.ReadAll(cs.req.Body) 232 c.Assert(err, check.IsNil) 233 234 c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") 235 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") 236 237 c.Check(cs.req.Method, check.Equals, "POST") 238 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") 239 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 240 _, ok := cs.req.Context().Deadline() 241 c.Assert(ok, check.Equals, false) 242 c.Check(id, check.Equals, "66b3") 243 } 244 245 func (cs *clientSuite) TestClientOpInstallPathIgnoreRunning(c *check.C) { 246 cs.status = 202 247 cs.rsp = `{ 248 "change": "66b3", 249 "status-code": 202, 250 "type": "async" 251 }` 252 bodyData := []byte("snap-data") 253 254 snap := filepath.Join(c.MkDir(), "foo.snap") 255 err := ioutil.WriteFile(snap, bodyData, 0644) 256 c.Assert(err, check.IsNil) 257 258 id, err := cs.cli.InstallPath(snap, "", &client.SnapOptions{IgnoreRunning: true}) 259 c.Assert(err, check.IsNil) 260 261 body, err := ioutil.ReadAll(cs.req.Body) 262 c.Assert(err, check.IsNil) 263 264 c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") 265 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") 266 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"ignore-running\"\r\n\r\ntrue\r\n.*") 267 268 c.Check(cs.req.Method, check.Equals, "POST") 269 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") 270 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 271 _, ok := cs.req.Context().Deadline() 272 c.Assert(ok, check.Equals, false) 273 c.Check(id, check.Equals, "66b3") 274 } 275 276 func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) { 277 cs.status = 202 278 cs.rsp = `{ 279 "change": "66b3", 280 "status-code": 202, 281 "type": "async" 282 }` 283 bodyData := []byte("snap-data") 284 285 snap := filepath.Join(c.MkDir(), "foo.snap") 286 err := ioutil.WriteFile(snap, bodyData, 0644) 287 c.Assert(err, check.IsNil) 288 289 id, err := cs.cli.InstallPath(snap, "foo_bar", nil) 290 c.Assert(err, check.IsNil) 291 292 body, err := ioutil.ReadAll(cs.req.Body) 293 c.Assert(err, check.IsNil) 294 295 c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") 296 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") 297 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"name\"\r\n\r\nfoo_bar\r\n.*") 298 299 c.Check(cs.req.Method, check.Equals, "POST") 300 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") 301 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 302 c.Check(id, check.Equals, "66b3") 303 } 304 305 func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { 306 cs.status = 202 307 cs.rsp = `{ 308 "change": "66b3", 309 "status-code": 202, 310 "type": "async" 311 }` 312 bodyData := []byte("snap-data") 313 314 snap := filepath.Join(c.MkDir(), "foo.snap") 315 err := ioutil.WriteFile(snap, bodyData, 0644) 316 c.Assert(err, check.IsNil) 317 318 opts := client.SnapOptions{ 319 Dangerous: true, 320 } 321 322 // InstallPath takes Dangerous 323 _, err = cs.cli.InstallPath(snap, "", &opts) 324 c.Assert(err, check.IsNil) 325 326 body, err := ioutil.ReadAll(cs.req.Body) 327 c.Assert(err, check.IsNil) 328 329 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*") 330 331 // Install does not (and gives us a clear error message) 332 _, err = cs.cli.Install("foo", &opts) 333 c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) 334 335 // nor does InstallMany (whether it fails because any option 336 // at all was provided, or because dangerous was provided, is 337 // unimportant) 338 _, err = cs.cli.InstallMany([]string{"foo"}, &opts) 339 c.Assert(err, check.NotNil) 340 } 341 342 func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) { 343 cs.status = 202 344 cs.rsp = `{ 345 "change": "66b3", 346 "status-code": 202, 347 "type": "async" 348 }` 349 bodyData := []byte("snap-data") 350 351 snap := filepath.Join(c.MkDir(), "foo.snap") 352 err := ioutil.WriteFile(snap, bodyData, 0644) 353 c.Assert(err, check.IsNil) 354 355 opts := client.SnapOptions{ 356 Unaliased: true, 357 } 358 359 _, err = cs.cli.Install("foo", &opts) 360 c.Assert(err, check.IsNil) 361 362 body, err := ioutil.ReadAll(cs.req.Body) 363 c.Assert(err, check.IsNil) 364 jsonBody := make(map[string]interface{}) 365 err = json.Unmarshal(body, &jsonBody) 366 c.Assert(err, check.IsNil, check.Commentf("body: %v", string(body))) 367 c.Check(jsonBody["unaliased"], check.Equals, true, check.Commentf("body: %v", string(body))) 368 369 _, err = cs.cli.InstallPath(snap, "", &opts) 370 c.Assert(err, check.IsNil) 371 372 body, err = ioutil.ReadAll(cs.req.Body) 373 c.Assert(err, check.IsNil) 374 375 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"unaliased\"\r\n\r\ntrue\r\n.*") 376 } 377 378 func formToMap(c *check.C, mr *multipart.Reader) map[string]string { 379 formData := map[string]string{} 380 for { 381 p, err := mr.NextPart() 382 if err == io.EOF { 383 break 384 } 385 c.Assert(err, check.IsNil) 386 slurp, err := ioutil.ReadAll(p) 387 c.Assert(err, check.IsNil) 388 formData[p.FormName()] = string(slurp) 389 } 390 return formData 391 } 392 393 func (cs *clientSuite) TestClientOpTryMode(c *check.C) { 394 cs.status = 202 395 cs.rsp = `{ 396 "change": "66b3", 397 "status-code": 202, 398 "type": "async" 399 }` 400 snapdir := filepath.Join(c.MkDir(), "/some/path") 401 402 for _, opts := range []*client.SnapOptions{ 403 {Classic: false, DevMode: false, JailMode: false}, 404 {Classic: false, DevMode: false, JailMode: true}, 405 {Classic: false, DevMode: true, JailMode: true}, 406 {Classic: false, DevMode: true, JailMode: false}, 407 {Classic: true, DevMode: false, JailMode: false}, 408 {Classic: true, DevMode: false, JailMode: true}, 409 {Classic: true, DevMode: true, JailMode: true}, 410 {Classic: true, DevMode: true, JailMode: false}, 411 } { 412 comment := check.Commentf("when Classic:%t DevMode:%t JailMode:%t", opts.Classic, opts.DevMode, opts.JailMode) 413 id, err := cs.cli.Try(snapdir, opts) 414 c.Assert(err, check.IsNil) 415 416 // ensure we send the right form-data 417 _, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type")) 418 c.Assert(err, check.IsNil, comment) 419 mr := multipart.NewReader(cs.req.Body, params["boundary"]) 420 formData := formToMap(c, mr) 421 c.Check(formData["action"], check.Equals, "try", comment) 422 c.Check(formData["snap-path"], check.Equals, snapdir, comment) 423 expectedLength := 2 424 if opts.Classic { 425 c.Check(formData["classic"], check.Equals, "true", comment) 426 expectedLength++ 427 } 428 if opts.DevMode { 429 c.Check(formData["devmode"], check.Equals, "true", comment) 430 expectedLength++ 431 } 432 if opts.JailMode { 433 c.Check(formData["jailmode"], check.Equals, "true", comment) 434 expectedLength++ 435 } 436 c.Check(len(formData), check.Equals, expectedLength) 437 438 c.Check(cs.req.Method, check.Equals, "POST", comment) 439 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", comment) 440 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*", comment) 441 c.Check(id, check.Equals, "66b3", comment) 442 } 443 } 444 445 func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) { 446 snapdir := filepath.Join(c.MkDir(), "/some/path") 447 448 _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) 449 c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) 450 } 451 452 func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) { 453 tests := map[string]client.SnapOptions{ 454 "{}": {}, 455 `{"channel":"edge"}`: {Channel: "edge"}, 456 `{"revision":"42"}`: {Revision: "42"}, 457 `{"cohort-key":"what"}`: {CohortKey: "what"}, 458 `{"leave-cohort":true}`: {LeaveCohort: true}, 459 `{"devmode":true}`: {DevMode: true}, 460 `{"jailmode":true}`: {JailMode: true}, 461 `{"classic":true}`: {Classic: true}, 462 `{"dangerous":true}`: {Dangerous: true}, 463 `{"ignore-validation":true}`: {IgnoreValidation: true}, 464 `{"unaliased":true}`: {Unaliased: true}, 465 `{"purge":true}`: {Purge: true}, 466 `{"amend":true}`: {Amend: true}, 467 } 468 for expected, opts := range tests { 469 buf, err := json.Marshal(&opts) 470 c.Assert(err, check.IsNil, check.Commentf("%s", expected)) 471 c.Check(string(buf), check.Equals, expected) 472 } 473 } 474 475 func (cs *clientSuite) TestClientOpDownload(c *check.C) { 476 cs.status = 200 477 cs.header = http.Header{ 478 "Content-Disposition": {"attachment; filename=foo_2.snap"}, 479 "Snap-Sha3-384": {"sha3sha3sha3"}, 480 "Snap-Download-Token": {"some-token"}, 481 } 482 cs.contentLength = 1234 483 484 cs.rsp = `lots-of-foo-data` 485 486 dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ 487 SnapOptions: client.SnapOptions{ 488 Revision: "2", 489 Channel: "edge", 490 }, 491 HeaderPeek: true, 492 }) 493 c.Check(err, check.IsNil) 494 c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ 495 SuggestedFileName: "foo_2.snap", 496 Size: 1234, 497 Sha3_384: "sha3sha3sha3", 498 ResumeToken: "some-token", 499 }) 500 501 // check we posted the right stuff 502 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") 503 c.Assert(cs.req.Header.Get("range"), check.Equals, "") 504 body, err := ioutil.ReadAll(cs.req.Body) 505 c.Assert(err, check.IsNil) 506 var jsonBody client.DownloadAction 507 err = json.Unmarshal(body, &jsonBody) 508 c.Assert(err, check.IsNil) 509 c.Check(jsonBody.SnapName, check.DeepEquals, "foo") 510 c.Check(jsonBody.Revision, check.Equals, "2") 511 c.Check(jsonBody.Channel, check.Equals, "edge") 512 c.Check(jsonBody.HeaderPeek, check.Equals, true) 513 514 // ensure we can read the response 515 content, err := ioutil.ReadAll(rc) 516 c.Assert(err, check.IsNil) 517 c.Check(string(content), check.Equals, cs.rsp) 518 // and we can close it 519 c.Check(rc.Close(), check.IsNil) 520 } 521 522 func (cs *clientSuite) TestClientOpDownloadResume(c *check.C) { 523 cs.status = 200 524 cs.header = http.Header{ 525 "Content-Disposition": {"attachment; filename=foo_2.snap"}, 526 "Snap-Sha3-384": {"sha3sha3sha3"}, 527 } 528 // we resume 529 cs.contentLength = 1234 - 64 530 531 cs.rsp = `lots-of-foo-data` 532 533 dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ 534 SnapOptions: client.SnapOptions{ 535 Revision: "2", 536 Channel: "edge", 537 }, 538 HeaderPeek: true, 539 ResumeToken: "some-token", 540 Resume: 64, 541 }) 542 c.Check(err, check.IsNil) 543 c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ 544 SuggestedFileName: "foo_2.snap", 545 Size: 1234 - 64, 546 Sha3_384: "sha3sha3sha3", 547 }) 548 549 // check we posted the right stuff 550 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") 551 c.Assert(cs.req.Header.Get("range"), check.Equals, "bytes: 64-") 552 body, err := ioutil.ReadAll(cs.req.Body) 553 c.Assert(err, check.IsNil) 554 var jsonBody client.DownloadAction 555 err = json.Unmarshal(body, &jsonBody) 556 c.Assert(err, check.IsNil) 557 c.Check(jsonBody.SnapName, check.DeepEquals, "foo") 558 c.Check(jsonBody.Revision, check.Equals, "2") 559 c.Check(jsonBody.Channel, check.Equals, "edge") 560 c.Check(jsonBody.HeaderPeek, check.Equals, true) 561 c.Check(jsonBody.ResumeToken, check.Equals, "some-token") 562 563 // ensure we can read the response 564 content, err := ioutil.ReadAll(rc) 565 c.Assert(err, check.IsNil) 566 c.Check(string(content), check.Equals, cs.rsp) 567 // and we can close it 568 c.Check(rc.Close(), check.IsNil) 569 }