github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 "path/filepath" 31 32 "gopkg.in/check.v1" 33 34 "github.com/snapcore/snapd/client" 35 ) 36 37 var chanName = "achan" 38 39 var ops = []struct { 40 op func(*client.Client, string, *client.SnapOptions) (string, error) 41 action string 42 }{ 43 {(*client.Client).Install, "install"}, 44 {(*client.Client).Refresh, "refresh"}, 45 {(*client.Client).Remove, "remove"}, 46 {(*client.Client).Revert, "revert"}, 47 {(*client.Client).Enable, "enable"}, 48 {(*client.Client).Disable, "disable"}, 49 {(*client.Client).Switch, "switch"}, 50 } 51 52 var multiOps = []struct { 53 op func(*client.Client, []string, *client.SnapOptions) (string, error) 54 action string 55 }{ 56 {(*client.Client).RefreshMany, "refresh"}, 57 {(*client.Client).InstallMany, "install"}, 58 {(*client.Client).RemoveMany, "remove"}, 59 } 60 61 func (cs *clientSuite) TestClientOpSnapServerError(c *check.C) { 62 cs.err = errors.New("fail") 63 for _, s := range ops { 64 _, err := s.op(cs.cli, pkgName, nil) 65 c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) 66 } 67 } 68 69 func (cs *clientSuite) TestClientMultiOpSnapServerError(c *check.C) { 70 cs.err = errors.New("fail") 71 for _, s := range multiOps { 72 _, err := s.op(cs.cli, nil, nil) 73 c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action)) 74 } 75 _, _, err := cs.cli.SnapshotMany(nil, nil) 76 c.Check(err, check.ErrorMatches, `.*fail`) 77 } 78 79 func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) { 80 cs.status = 400 81 cs.rsp = `{"type": "error"}` 82 for _, s := range ops { 83 _, err := s.op(cs.cli, pkgName, nil) 84 c.Check(err, check.ErrorMatches, `.*server error: "Bad Request"`, check.Commentf(s.action)) 85 } 86 } 87 88 func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) { 89 cs.status = 500 90 cs.rsp = `{"type": "error"}` 91 for _, s := range multiOps { 92 _, err := s.op(cs.cli, nil, nil) 93 c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`, check.Commentf(s.action)) 94 } 95 _, _, err := cs.cli.SnapshotMany(nil, nil) 96 c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`) 97 } 98 99 func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) { 100 cs.rsp = `{"type": "what"}` 101 for _, s := range ops { 102 _, err := s.op(cs.cli, pkgName, nil) 103 c.Check(err, check.ErrorMatches, `.*expected async response for "POST" on "/v2/snaps/`+pkgName+`", got "what"`, check.Commentf(s.action)) 104 } 105 } 106 107 func (cs *clientSuite) TestClientOpSnapNotAccepted(c *check.C) { 108 cs.rsp = `{ 109 "status-code": 200, 110 "type": "async" 111 }` 112 for _, s := range ops { 113 _, err := s.op(cs.cli, pkgName, nil) 114 c.Check(err, check.ErrorMatches, `.*operation not accepted`, check.Commentf(s.action)) 115 } 116 } 117 118 func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) { 119 cs.status = 202 120 cs.rsp = `{ 121 "status-code": 202, 122 "type": "async" 123 }` 124 for _, s := range ops { 125 _, err := s.op(cs.cli, pkgName, nil) 126 c.Assert(err, check.ErrorMatches, `.*response without change reference.*`, check.Commentf(s.action)) 127 } 128 } 129 130 func (cs *clientSuite) TestClientOpSnap(c *check.C) { 131 cs.status = 202 132 cs.rsp = `{ 133 "change": "d728", 134 "status-code": 202, 135 "type": "async" 136 }` 137 for _, s := range ops { 138 id, err := s.op(cs.cli, pkgName, &client.SnapOptions{Channel: chanName}) 139 c.Assert(err, check.IsNil) 140 141 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) 142 143 body, err := ioutil.ReadAll(cs.req.Body) 144 c.Assert(err, check.IsNil, check.Commentf(s.action)) 145 jsonBody := make(map[string]string) 146 err = json.Unmarshal(body, &jsonBody) 147 c.Assert(err, check.IsNil, check.Commentf(s.action)) 148 c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) 149 c.Check(jsonBody["channel"], check.Equals, chanName, check.Commentf(s.action)) 150 c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) 151 152 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName), check.Commentf(s.action)) 153 c.Check(id, check.Equals, "d728", check.Commentf(s.action)) 154 } 155 } 156 157 func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) { 158 cs.status = 202 159 cs.rsp = `{ 160 "change": "d728", 161 "status-code": 202, 162 "type": "async" 163 }` 164 for _, s := range multiOps { 165 // Note body is essentially the same as TestClientMultiSnapshot; keep in sync 166 id, err := s.op(cs.cli, []string{pkgName}, nil) 167 c.Assert(err, check.IsNil) 168 169 c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) 170 171 body, err := ioutil.ReadAll(cs.req.Body) 172 c.Assert(err, check.IsNil, check.Commentf(s.action)) 173 jsonBody := make(map[string]interface{}) 174 err = json.Unmarshal(body, &jsonBody) 175 c.Assert(err, check.IsNil, check.Commentf(s.action)) 176 c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action)) 177 c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}, check.Commentf(s.action)) 178 c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action)) 179 180 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", check.Commentf(s.action)) 181 c.Check(id, check.Equals, "d728", check.Commentf(s.action)) 182 } 183 } 184 185 func (cs *clientSuite) TestClientMultiSnapshot(c *check.C) { 186 // Note body is essentially the same as TestClientMultiOpSnap; keep in sync 187 cs.status = 202 188 cs.rsp = `{ 189 "result": {"set-id": 42}, 190 "change": "d728", 191 "status-code": 202, 192 "type": "async" 193 }` 194 setID, changeID, err := cs.cli.SnapshotMany([]string{pkgName}, nil) 195 c.Assert(err, check.IsNil) 196 c.Check(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") 197 198 body, err := ioutil.ReadAll(cs.req.Body) 199 c.Assert(err, check.IsNil) 200 jsonBody := make(map[string]interface{}) 201 err = json.Unmarshal(body, &jsonBody) 202 c.Assert(err, check.IsNil) 203 c.Check(jsonBody["action"], check.Equals, "snapshot") 204 c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}) 205 c.Check(jsonBody, check.HasLen, 2) 206 c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps") 207 c.Check(setID, check.Equals, uint64(42)) 208 c.Check(changeID, check.Equals, "d728") 209 } 210 211 func (cs *clientSuite) TestClientOpInstallPath(c *check.C) { 212 cs.status = 202 213 cs.rsp = `{ 214 "change": "66b3", 215 "status-code": 202, 216 "type": "async" 217 }` 218 bodyData := []byte("snap-data") 219 220 snap := filepath.Join(c.MkDir(), "foo.snap") 221 err := ioutil.WriteFile(snap, bodyData, 0644) 222 c.Assert(err, check.IsNil) 223 224 id, err := cs.cli.InstallPath(snap, "", nil) 225 c.Assert(err, check.IsNil) 226 227 body, err := ioutil.ReadAll(cs.req.Body) 228 c.Assert(err, check.IsNil) 229 230 c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") 231 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") 232 233 c.Check(cs.req.Method, check.Equals, "POST") 234 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) 235 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 236 c.Check(id, check.Equals, "66b3") 237 } 238 239 func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) { 240 cs.status = 202 241 cs.rsp = `{ 242 "change": "66b3", 243 "status-code": 202, 244 "type": "async" 245 }` 246 bodyData := []byte("snap-data") 247 248 snap := filepath.Join(c.MkDir(), "foo.snap") 249 err := ioutil.WriteFile(snap, bodyData, 0644) 250 c.Assert(err, check.IsNil) 251 252 id, err := cs.cli.InstallPath(snap, "foo_bar", nil) 253 c.Assert(err, check.IsNil) 254 255 body, err := ioutil.ReadAll(cs.req.Body) 256 c.Assert(err, check.IsNil) 257 258 c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*") 259 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*") 260 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"name\"\r\n\r\nfoo_bar\r\n.*") 261 262 c.Check(cs.req.Method, check.Equals, "POST") 263 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) 264 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") 265 c.Check(id, check.Equals, "66b3") 266 } 267 268 func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { 269 cs.status = 202 270 cs.rsp = `{ 271 "change": "66b3", 272 "status-code": 202, 273 "type": "async" 274 }` 275 bodyData := []byte("snap-data") 276 277 snap := filepath.Join(c.MkDir(), "foo.snap") 278 err := ioutil.WriteFile(snap, bodyData, 0644) 279 c.Assert(err, check.IsNil) 280 281 opts := client.SnapOptions{ 282 Dangerous: true, 283 } 284 285 // InstallPath takes Dangerous 286 _, err = cs.cli.InstallPath(snap, "", &opts) 287 c.Assert(err, check.IsNil) 288 289 body, err := ioutil.ReadAll(cs.req.Body) 290 c.Assert(err, check.IsNil) 291 292 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*") 293 294 // Install does not (and gives us a clear error message) 295 _, err = cs.cli.Install("foo", &opts) 296 c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) 297 298 // nor does InstallMany (whether it fails because any option 299 // at all was provided, or because dangerous was provided, is 300 // unimportant) 301 _, err = cs.cli.InstallMany([]string{"foo"}, &opts) 302 c.Assert(err, check.NotNil) 303 } 304 305 func (cs *clientSuite) TestClientOpInstallUnaliased(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 Unaliased: true, 320 } 321 322 _, err = cs.cli.Install("foo", &opts) 323 c.Assert(err, check.IsNil) 324 325 body, err := ioutil.ReadAll(cs.req.Body) 326 c.Assert(err, check.IsNil) 327 jsonBody := make(map[string]interface{}) 328 err = json.Unmarshal(body, &jsonBody) 329 c.Assert(err, check.IsNil, check.Commentf("body: %v", string(body))) 330 c.Check(jsonBody["unaliased"], check.Equals, true, check.Commentf("body: %v", string(body))) 331 332 _, err = cs.cli.InstallPath(snap, "", &opts) 333 c.Assert(err, check.IsNil) 334 335 body, err = ioutil.ReadAll(cs.req.Body) 336 c.Assert(err, check.IsNil) 337 338 c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"unaliased\"\r\n\r\ntrue\r\n.*") 339 } 340 341 func formToMap(c *check.C, mr *multipart.Reader) map[string]string { 342 formData := map[string]string{} 343 for { 344 p, err := mr.NextPart() 345 if err == io.EOF { 346 break 347 } 348 c.Assert(err, check.IsNil) 349 slurp, err := ioutil.ReadAll(p) 350 c.Assert(err, check.IsNil) 351 formData[p.FormName()] = string(slurp) 352 } 353 return formData 354 } 355 356 func (cs *clientSuite) TestClientOpTryMode(c *check.C) { 357 cs.status = 202 358 cs.rsp = `{ 359 "change": "66b3", 360 "status-code": 202, 361 "type": "async" 362 }` 363 snapdir := filepath.Join(c.MkDir(), "/some/path") 364 365 for _, opts := range []*client.SnapOptions{ 366 {Classic: false, DevMode: false, JailMode: false}, 367 {Classic: false, DevMode: false, JailMode: true}, 368 {Classic: false, DevMode: true, JailMode: true}, 369 {Classic: false, DevMode: true, JailMode: false}, 370 {Classic: true, DevMode: false, JailMode: false}, 371 {Classic: true, DevMode: false, JailMode: true}, 372 {Classic: true, DevMode: true, JailMode: true}, 373 {Classic: true, DevMode: true, JailMode: false}, 374 } { 375 comment := check.Commentf("when Classic:%t DevMode:%t JailMode:%t", opts.Classic, opts.DevMode, opts.JailMode) 376 id, err := cs.cli.Try(snapdir, opts) 377 c.Assert(err, check.IsNil) 378 379 // ensure we send the right form-data 380 _, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type")) 381 c.Assert(err, check.IsNil, comment) 382 mr := multipart.NewReader(cs.req.Body, params["boundary"]) 383 formData := formToMap(c, mr) 384 c.Check(formData["action"], check.Equals, "try", comment) 385 c.Check(formData["snap-path"], check.Equals, snapdir, comment) 386 expectedLength := 2 387 if opts.Classic { 388 c.Check(formData["classic"], check.Equals, "true", comment) 389 expectedLength++ 390 } 391 if opts.DevMode { 392 c.Check(formData["devmode"], check.Equals, "true", comment) 393 expectedLength++ 394 } 395 if opts.JailMode { 396 c.Check(formData["jailmode"], check.Equals, "true", comment) 397 expectedLength++ 398 } 399 c.Check(len(formData), check.Equals, expectedLength) 400 401 c.Check(cs.req.Method, check.Equals, "POST", comment) 402 c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"), comment) 403 c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*", comment) 404 c.Check(id, check.Equals, "66b3", comment) 405 } 406 } 407 408 func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) { 409 snapdir := filepath.Join(c.MkDir(), "/some/path") 410 411 _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) 412 c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) 413 } 414 415 func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) { 416 tests := map[string]client.SnapOptions{ 417 "{}": {}, 418 `{"channel":"edge"}`: {Channel: "edge"}, 419 `{"revision":"42"}`: {Revision: "42"}, 420 `{"cohort-key":"what"}`: {CohortKey: "what"}, 421 `{"leave-cohort":true}`: {LeaveCohort: true}, 422 `{"devmode":true}`: {DevMode: true}, 423 `{"jailmode":true}`: {JailMode: true}, 424 `{"classic":true}`: {Classic: true}, 425 `{"dangerous":true}`: {Dangerous: true}, 426 `{"ignore-validation":true}`: {IgnoreValidation: true}, 427 `{"unaliased":true}`: {Unaliased: true}, 428 `{"purge":true}`: {Purge: true}, 429 `{"amend":true}`: {Amend: true}, 430 } 431 for expected, opts := range tests { 432 buf, err := json.Marshal(&opts) 433 c.Assert(err, check.IsNil, check.Commentf("%s", expected)) 434 c.Check(string(buf), check.Equals, expected) 435 } 436 }