github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/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, fmt.Sprintf("/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) TestClientOpInstallPathInstance(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, "foo_bar", nil) 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=\"name\"\r\n\r\nfoo_bar\r\n.*") 267 268 c.Check(cs.req.Method, check.Equals, "POST") 269 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) 270 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 271 c.Check(id, check.Equals, "66b3") 272 } 273 274 func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { 275 cs.status = 202 276 cs.rsp = `{ 277 "change": "66b3", 278 "status-code": 202, 279 "type": "async" 280 }` 281 bodyData := []byte("snap-data") 282 283 snap := filepath.Join(c.MkDir(), "foo.snap") 284 err := ioutil.WriteFile(snap, bodyData, 0644) 285 c.Assert(err, check.IsNil) 286 287 opts := client.SnapOptions{ 288 Dangerous: true, 289 } 290 291 // InstallPath takes Dangerous 292 _, err = cs.cli.InstallPath(snap, "", &opts) 293 c.Assert(err, check.IsNil) 294 295 body, err := ioutil.ReadAll(cs.req.Body) 296 c.Assert(err, check.IsNil) 297 298 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*") 299 300 // Install does not (and gives us a clear error message) 301 _, err = cs.cli.Install("foo", &opts) 302 c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) 303 304 // nor does InstallMany (whether it fails because any option 305 // at all was provided, or because dangerous was provided, is 306 // unimportant) 307 _, err = cs.cli.InstallMany([]string{"foo"}, &opts) 308 c.Assert(err, check.NotNil) 309 } 310 311 func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) { 312 cs.status = 202 313 cs.rsp = `{ 314 "change": "66b3", 315 "status-code": 202, 316 "type": "async" 317 }` 318 bodyData := []byte("snap-data") 319 320 snap := filepath.Join(c.MkDir(), "foo.snap") 321 err := ioutil.WriteFile(snap, bodyData, 0644) 322 c.Assert(err, check.IsNil) 323 324 opts := client.SnapOptions{ 325 Unaliased: true, 326 } 327 328 _, err = cs.cli.Install("foo", &opts) 329 c.Assert(err, check.IsNil) 330 331 body, err := ioutil.ReadAll(cs.req.Body) 332 c.Assert(err, check.IsNil) 333 jsonBody := make(map[string]interface{}) 334 err = json.Unmarshal(body, &jsonBody) 335 c.Assert(err, check.IsNil, check.Commentf("body: %v", string(body))) 336 c.Check(jsonBody["unaliased"], check.Equals, true, check.Commentf("body: %v", string(body))) 337 338 _, err = cs.cli.InstallPath(snap, "", &opts) 339 c.Assert(err, check.IsNil) 340 341 body, err = ioutil.ReadAll(cs.req.Body) 342 c.Assert(err, check.IsNil) 343 344 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"unaliased\"\r\n\r\ntrue\r\n.*") 345 } 346 347 func formToMap(c *check.C, mr *multipart.Reader) map[string]string { 348 formData := map[string]string{} 349 for { 350 p, err := mr.NextPart() 351 if err == io.EOF { 352 break 353 } 354 c.Assert(err, check.IsNil) 355 slurp, err := ioutil.ReadAll(p) 356 c.Assert(err, check.IsNil) 357 formData[p.FormName()] = string(slurp) 358 } 359 return formData 360 } 361 362 func (cs *clientSuite) TestClientOpTryMode(c *check.C) { 363 cs.status = 202 364 cs.rsp = `{ 365 "change": "66b3", 366 "status-code": 202, 367 "type": "async" 368 }` 369 snapdir := filepath.Join(c.MkDir(), "/some/path") 370 371 for _, opts := range []*client.SnapOptions{ 372 {Classic: false, DevMode: false, JailMode: false}, 373 {Classic: false, DevMode: false, JailMode: true}, 374 {Classic: false, DevMode: true, JailMode: true}, 375 {Classic: false, DevMode: true, JailMode: false}, 376 {Classic: true, DevMode: false, JailMode: false}, 377 {Classic: true, DevMode: false, JailMode: true}, 378 {Classic: true, DevMode: true, JailMode: true}, 379 {Classic: true, DevMode: true, JailMode: false}, 380 } { 381 comment := check.Commentf("when Classic:%t DevMode:%t JailMode:%t", opts.Classic, opts.DevMode, opts.JailMode) 382 id, err := cs.cli.Try(snapdir, opts) 383 c.Assert(err, check.IsNil) 384 385 // ensure we send the right form-data 386 _, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type")) 387 c.Assert(err, check.IsNil, comment) 388 mr := multipart.NewReader(cs.req.Body, params["boundary"]) 389 formData := formToMap(c, mr) 390 c.Check(formData["action"], check.Equals, "try", comment) 391 c.Check(formData["snap-path"], check.Equals, snapdir, comment) 392 expectedLength := 2 393 if opts.Classic { 394 c.Check(formData["classic"], check.Equals, "true", comment) 395 expectedLength++ 396 } 397 if opts.DevMode { 398 c.Check(formData["devmode"], check.Equals, "true", comment) 399 expectedLength++ 400 } 401 if opts.JailMode { 402 c.Check(formData["jailmode"], check.Equals, "true", comment) 403 expectedLength++ 404 } 405 c.Check(len(formData), check.Equals, expectedLength) 406 407 c.Check(cs.req.Method, check.Equals, "POST", comment) 408 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"), comment) 409 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*", comment) 410 c.Check(id, check.Equals, "66b3", comment) 411 } 412 } 413 414 func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) { 415 snapdir := filepath.Join(c.MkDir(), "/some/path") 416 417 _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) 418 c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) 419 } 420 421 func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) { 422 tests := map[string]client.SnapOptions{ 423 "{}": {}, 424 `{"channel":"edge"}`: {Channel: "edge"}, 425 `{"revision":"42"}`: {Revision: "42"}, 426 `{"cohort-key":"what"}`: {CohortKey: "what"}, 427 `{"leave-cohort":true}`: {LeaveCohort: true}, 428 `{"devmode":true}`: {DevMode: true}, 429 `{"jailmode":true}`: {JailMode: true}, 430 `{"classic":true}`: {Classic: true}, 431 `{"dangerous":true}`: {Dangerous: true}, 432 `{"ignore-validation":true}`: {IgnoreValidation: true}, 433 `{"unaliased":true}`: {Unaliased: true}, 434 `{"purge":true}`: {Purge: true}, 435 `{"amend":true}`: {Amend: true}, 436 } 437 for expected, opts := range tests { 438 buf, err := json.Marshal(&opts) 439 c.Assert(err, check.IsNil, check.Commentf("%s", expected)) 440 c.Check(string(buf), check.Equals, expected) 441 } 442 } 443 444 func (cs *clientSuite) TestClientOpDownload(c *check.C) { 445 cs.status = 200 446 cs.header = http.Header{ 447 "Content-Disposition": {"attachment; filename=foo_2.snap"}, 448 "Snap-Sha3-384": {"sha3sha3sha3"}, 449 "Snap-Download-Token": {"some-token"}, 450 } 451 cs.contentLength = 1234 452 453 cs.rsp = `lots-of-foo-data` 454 455 dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ 456 SnapOptions: client.SnapOptions{ 457 Revision: "2", 458 Channel: "edge", 459 }, 460 HeaderPeek: true, 461 }) 462 c.Check(err, check.IsNil) 463 c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ 464 SuggestedFileName: "foo_2.snap", 465 Size: 1234, 466 Sha3_384: "sha3sha3sha3", 467 ResumeToken: "some-token", 468 }) 469 470 // check we posted the right stuff 471 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") 472 c.Assert(cs.req.Header.Get("range"), check.Equals, "") 473 body, err := ioutil.ReadAll(cs.req.Body) 474 c.Assert(err, check.IsNil) 475 var jsonBody client.DownloadAction 476 err = json.Unmarshal(body, &jsonBody) 477 c.Assert(err, check.IsNil) 478 c.Check(jsonBody.SnapName, check.DeepEquals, "foo") 479 c.Check(jsonBody.Revision, check.Equals, "2") 480 c.Check(jsonBody.Channel, check.Equals, "edge") 481 c.Check(jsonBody.HeaderPeek, check.Equals, true) 482 483 // ensure we can read the response 484 content, err := ioutil.ReadAll(rc) 485 c.Assert(err, check.IsNil) 486 c.Check(string(content), check.Equals, cs.rsp) 487 // and we can close it 488 c.Check(rc.Close(), check.IsNil) 489 } 490 491 func (cs *clientSuite) TestClientOpDownloadResume(c *check.C) { 492 cs.status = 200 493 cs.header = http.Header{ 494 "Content-Disposition": {"attachment; filename=foo_2.snap"}, 495 "Snap-Sha3-384": {"sha3sha3sha3"}, 496 } 497 // we resume 498 cs.contentLength = 1234 - 64 499 500 cs.rsp = `lots-of-foo-data` 501 502 dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ 503 SnapOptions: client.SnapOptions{ 504 Revision: "2", 505 Channel: "edge", 506 }, 507 HeaderPeek: true, 508 ResumeToken: "some-token", 509 Resume: 64, 510 }) 511 c.Check(err, check.IsNil) 512 c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ 513 SuggestedFileName: "foo_2.snap", 514 Size: 1234 - 64, 515 Sha3_384: "sha3sha3sha3", 516 }) 517 518 // check we posted the right stuff 519 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") 520 c.Assert(cs.req.Header.Get("range"), check.Equals, "bytes: 64-") 521 body, err := ioutil.ReadAll(cs.req.Body) 522 c.Assert(err, check.IsNil) 523 var jsonBody client.DownloadAction 524 err = json.Unmarshal(body, &jsonBody) 525 c.Assert(err, check.IsNil) 526 c.Check(jsonBody.SnapName, check.DeepEquals, "foo") 527 c.Check(jsonBody.Revision, check.Equals, "2") 528 c.Check(jsonBody.Channel, check.Equals, "edge") 529 c.Check(jsonBody.HeaderPeek, check.Equals, true) 530 c.Check(jsonBody.ResumeToken, check.Equals, "some-token") 531 532 // ensure we can read the response 533 content, err := ioutil.ReadAll(rc) 534 c.Assert(err, check.IsNil) 535 c.Check(string(content), check.Equals, cs.rsp) 536 // and we can close it 537 c.Check(rc.Close(), check.IsNil) 538 }