github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/store/store_action_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2020 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 store_test 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "net/http" 28 "net/http/httptest" 29 "net/url" 30 "os" 31 "strings" 32 "time" 33 34 . "gopkg.in/check.v1" 35 36 "github.com/snapcore/snapd/arch" 37 "github.com/snapcore/snapd/release" 38 "github.com/snapcore/snapd/snap" 39 "github.com/snapcore/snapd/snap/channel" 40 "github.com/snapcore/snapd/snap/snaptest" 41 "github.com/snapcore/snapd/store" 42 "github.com/snapcore/snapd/testutil" 43 ) 44 45 type storeActionSuite struct { 46 baseStoreSuite 47 48 mockXDelta *testutil.MockCmd 49 } 50 51 var _ = Suite(&storeActionSuite{}) 52 53 func (s *storeActionSuite) SetUpTest(c *C) { 54 s.baseStoreSuite.SetUpTest(c) 55 56 s.mockXDelta = testutil.MockCommand(c, "xdelta3", "") 57 s.AddCleanup(s.mockXDelta.Restore) 58 } 59 60 var ( 61 helloRefreshedDateStr = "2018-02-27T11:00:00Z" 62 helloRefreshedDate time.Time 63 ) 64 65 func init() { 66 t, err := time.Parse(time.RFC3339, helloRefreshedDateStr) 67 if err != nil { 68 panic(err) 69 } 70 helloRefreshedDate = t 71 } 72 73 const helloCohortKey = "this is a very short cohort key, as cohort keys go, because those are *long*" 74 75 func (s *storeActionSuite) TestSnapAction(c *C) { 76 restore := release.MockOnClassic(false) 77 defer restore() 78 79 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 80 assertRequest(c, r, "POST", snapActionPath) 81 // check device authorization is set, implicitly checking doRequest was used 82 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 83 84 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 85 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "") 86 87 // no store ID by default 88 storeID := r.Header.Get("Snap-Device-Store") 89 c.Check(storeID, Equals, "") 90 91 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 92 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 93 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 94 95 jsonReq, err := ioutil.ReadAll(r.Body) 96 c.Assert(err, IsNil) 97 var req struct { 98 Context []map[string]interface{} `json:"context"` 99 Fields []string `json:"fields"` 100 Actions []map[string]interface{} `json:"actions"` 101 } 102 103 err = json.Unmarshal(jsonReq, &req) 104 c.Assert(err, IsNil) 105 106 c.Check(req.Fields, DeepEquals, store.SnapActionFields) 107 108 c.Assert(req.Context, HasLen, 1) 109 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 110 "snap-id": helloWorldSnapID, 111 "instance-key": helloWorldSnapID, 112 "revision": float64(1), 113 "tracking-channel": "beta", 114 "refreshed-date": helloRefreshedDateStr, 115 "epoch": iZeroEpoch, 116 }) 117 c.Assert(req.Actions, HasLen, 1) 118 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 119 "action": "refresh", 120 "instance-key": helloWorldSnapID, 121 "snap-id": helloWorldSnapID, 122 "cohort-key": helloCohortKey, 123 }) 124 125 io.WriteString(w, `{ 126 "results": [{ 127 "result": "refresh", 128 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 129 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 130 "name": "hello-world", 131 "snap": { 132 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 133 "name": "hello-world", 134 "revision": 26, 135 "version": "6.1", 136 "epoch": {"read": [0], "write": [0]}, 137 "publisher": { 138 "id": "canonical", 139 "username": "canonical", 140 "display-name": "Canonical" 141 } 142 } 143 }] 144 }`) 145 })) 146 147 c.Assert(mockServer, NotNil) 148 defer mockServer.Close() 149 150 mockServerURL, _ := url.Parse(mockServer.URL) 151 cfg := store.Config{ 152 StoreBaseURL: mockServerURL, 153 } 154 dauthCtx := &testDauthContext{c: c, device: s.device} 155 sto := store.New(&cfg, dauthCtx) 156 157 results, aresults, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 158 { 159 InstanceName: "hello-world", 160 SnapID: helloWorldSnapID, 161 TrackingChannel: "beta", 162 Revision: snap.R(1), 163 RefreshedDate: helloRefreshedDate, 164 }, 165 }, []*store.SnapAction{ 166 { 167 Action: "refresh", 168 SnapID: helloWorldSnapID, 169 InstanceName: "hello-world", 170 CohortKey: helloCohortKey, 171 }, 172 }, nil, nil, nil) 173 c.Assert(err, IsNil) 174 c.Assert(aresults, HasLen, 0) 175 c.Assert(results, HasLen, 1) 176 c.Assert(results[0].InstanceName(), Equals, "hello-world") 177 c.Assert(results[0].Revision, Equals, snap.R(26)) 178 c.Assert(results[0].Version, Equals, "6.1") 179 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 180 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 181 c.Assert(results[0].Deltas, HasLen, 0) 182 c.Assert(results[0].Epoch, DeepEquals, snap.E("0")) 183 } 184 185 func (s *storeActionSuite) TestSnapActionNonZeroEpochAndEpochBump(c *C) { 186 restore := release.MockOnClassic(false) 187 defer restore() 188 189 numReqs := 0 190 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 191 numReqs++ 192 assertRequest(c, r, "POST", snapActionPath) 193 // check device authorization is set, implicitly checking doRequest was used 194 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 195 196 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 197 198 // no store ID by default 199 storeID := r.Header.Get("Snap-Device-Store") 200 c.Check(storeID, Equals, "") 201 202 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 203 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 204 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 205 206 jsonReq, err := ioutil.ReadAll(r.Body) 207 c.Assert(err, IsNil) 208 var req struct { 209 Context []map[string]interface{} `json:"context"` 210 Fields []string `json:"fields"` 211 Actions []map[string]interface{} `json:"actions"` 212 } 213 214 err = json.Unmarshal(jsonReq, &req) 215 c.Assert(err, IsNil) 216 217 c.Check(req.Fields, DeepEquals, store.SnapActionFields) 218 219 c.Assert(req.Context, HasLen, 1) 220 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 221 "snap-id": helloWorldSnapID, 222 "instance-key": helloWorldSnapID, 223 "revision": float64(1), 224 "tracking-channel": "beta", 225 "refreshed-date": helloRefreshedDateStr, 226 "epoch": iFiveStarEpoch, 227 }) 228 c.Assert(req.Actions, HasLen, 1) 229 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 230 "action": "refresh", 231 "instance-key": helloWorldSnapID, 232 "snap-id": helloWorldSnapID, 233 }) 234 235 io.WriteString(w, `{ 236 "results": [{ 237 "result": "refresh", 238 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 239 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 240 "name": "hello-world", 241 "snap": { 242 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 243 "name": "hello-world", 244 "revision": 26, 245 "version": "6.1", 246 "epoch": {"read": [5, 6], "write": [6]}, 247 "publisher": { 248 "id": "canonical", 249 "username": "canonical", 250 "display-name": "Canonical" 251 } 252 } 253 }] 254 }`) 255 })) 256 257 c.Assert(mockServer, NotNil) 258 defer mockServer.Close() 259 260 mockServerURL, _ := url.Parse(mockServer.URL) 261 cfg := store.Config{ 262 StoreBaseURL: mockServerURL, 263 } 264 dauthCtx := &testDauthContext{c: c, device: s.device} 265 sto := store.New(&cfg, dauthCtx) 266 267 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 268 { 269 InstanceName: "hello-world", 270 SnapID: helloWorldSnapID, 271 TrackingChannel: "beta", 272 Revision: snap.R(1), 273 RefreshedDate: helloRefreshedDate, 274 Epoch: snap.E("5*"), 275 }, 276 }, []*store.SnapAction{ 277 { 278 Action: "refresh", 279 SnapID: helloWorldSnapID, 280 InstanceName: "hello-world", 281 }, 282 }, nil, nil, nil) 283 c.Assert(err, IsNil) 284 c.Assert(results, HasLen, 1) 285 c.Assert(results[0].InstanceName(), Equals, "hello-world") 286 c.Assert(results[0].Revision, Equals, snap.R(26)) 287 c.Assert(results[0].Version, Equals, "6.1") 288 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 289 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 290 c.Assert(results[0].Deltas, HasLen, 0) 291 c.Assert(results[0].Epoch, DeepEquals, snap.E("6*")) 292 293 c.Assert(numReqs, Equals, 1) // should be >1 soon :-) 294 } 295 296 func (s *storeActionSuite) TestSnapActionNoResults(c *C) { 297 restore := release.MockOnClassic(false) 298 defer restore() 299 300 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 301 assertRequest(c, r, "POST", snapActionPath) 302 // check device authorization is set, implicitly checking doRequest was used 303 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 304 305 jsonReq, err := ioutil.ReadAll(r.Body) 306 c.Assert(err, IsNil) 307 var req struct { 308 Context []map[string]interface{} `json:"context"` 309 Actions []map[string]interface{} `json:"actions"` 310 } 311 312 err = json.Unmarshal(jsonReq, &req) 313 c.Assert(err, IsNil) 314 315 c.Assert(req.Context, HasLen, 1) 316 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 317 "snap-id": helloWorldSnapID, 318 "instance-key": helloWorldSnapID, 319 "revision": float64(1), 320 "tracking-channel": "beta", 321 "refreshed-date": helloRefreshedDateStr, 322 "epoch": iZeroEpoch, 323 }) 324 c.Assert(req.Actions, HasLen, 0) 325 io.WriteString(w, `{ 326 "results": [] 327 }`) 328 })) 329 330 c.Assert(mockServer, NotNil) 331 defer mockServer.Close() 332 333 mockServerURL, _ := url.Parse(mockServer.URL) 334 cfg := store.Config{ 335 StoreBaseURL: mockServerURL, 336 } 337 dauthCtx := &testDauthContext{c: c, device: s.device} 338 sto := store.New(&cfg, dauthCtx) 339 340 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 341 { 342 InstanceName: "hello-world", 343 SnapID: helloWorldSnapID, 344 TrackingChannel: "beta", 345 Revision: snap.R(1), 346 RefreshedDate: helloRefreshedDate, 347 }, 348 }, nil, nil, nil, nil) 349 c.Check(results, HasLen, 0) 350 c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true}) 351 352 // local no-op 353 results, _, err = sto.SnapAction(s.ctx, nil, nil, nil, nil, nil) 354 c.Check(results, HasLen, 0) 355 c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true}) 356 357 c.Check(err.Error(), Equals, "no install/refresh information results from the store") 358 } 359 360 func (s *storeActionSuite) TestSnapActionRefreshedDateIsOptional(c *C) { 361 restore := release.MockOnClassic(false) 362 defer restore() 363 364 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 365 assertRequest(c, r, "POST", snapActionPath) 366 // check device authorization is set, implicitly checking doRequest was used 367 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 368 369 jsonReq, err := ioutil.ReadAll(r.Body) 370 c.Assert(err, IsNil) 371 var req struct { 372 Context []map[string]interface{} `json:"context"` 373 Actions []map[string]interface{} `json:"actions"` 374 } 375 376 err = json.Unmarshal(jsonReq, &req) 377 c.Assert(err, IsNil) 378 379 c.Assert(req.Context, HasLen, 1) 380 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 381 "snap-id": helloWorldSnapID, 382 "instance-key": helloWorldSnapID, 383 384 "revision": float64(1), 385 "tracking-channel": "beta", 386 "epoch": iZeroEpoch, 387 }) 388 c.Assert(req.Actions, HasLen, 0) 389 io.WriteString(w, `{ 390 "results": [] 391 }`) 392 })) 393 394 c.Assert(mockServer, NotNil) 395 defer mockServer.Close() 396 397 mockServerURL, _ := url.Parse(mockServer.URL) 398 cfg := store.Config{ 399 StoreBaseURL: mockServerURL, 400 } 401 dauthCtx := &testDauthContext{c: c, device: s.device} 402 sto := store.New(&cfg, dauthCtx) 403 404 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 405 { 406 InstanceName: "hello-world", 407 SnapID: helloWorldSnapID, 408 TrackingChannel: "beta", 409 Revision: snap.R(1), 410 }, 411 }, nil, nil, nil, nil) 412 c.Check(results, HasLen, 0) 413 c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true}) 414 } 415 416 func (s *storeActionSuite) TestSnapActionSkipBlocked(c *C) { 417 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 418 assertRequest(c, r, "POST", snapActionPath) 419 // check device authorization is set, implicitly checking doRequest was used 420 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 421 422 jsonReq, err := ioutil.ReadAll(r.Body) 423 c.Assert(err, IsNil) 424 var req struct { 425 Context []map[string]interface{} `json:"context"` 426 Actions []map[string]interface{} `json:"actions"` 427 } 428 429 err = json.Unmarshal(jsonReq, &req) 430 c.Assert(err, IsNil) 431 432 c.Assert(req.Context, HasLen, 1) 433 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 434 "snap-id": helloWorldSnapID, 435 "instance-key": helloWorldSnapID, 436 "revision": float64(1), 437 "tracking-channel": "stable", 438 "refreshed-date": helloRefreshedDateStr, 439 "epoch": iZeroEpoch, 440 }) 441 c.Assert(req.Actions, HasLen, 1) 442 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 443 "action": "refresh", 444 "instance-key": helloWorldSnapID, 445 "snap-id": helloWorldSnapID, 446 "channel": "stable", 447 }) 448 449 io.WriteString(w, `{ 450 "results": [{ 451 "result": "refresh", 452 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 453 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 454 "name": "hello-world", 455 "snap": { 456 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 457 "name": "hello-world", 458 "revision": 26, 459 "version": "6.1", 460 "publisher": { 461 "id": "canonical", 462 "username": "canonical", 463 "display-name": "Canonical" 464 } 465 } 466 }] 467 }`) 468 })) 469 470 c.Assert(mockServer, NotNil) 471 defer mockServer.Close() 472 473 mockServerURL, _ := url.Parse(mockServer.URL) 474 cfg := store.Config{ 475 StoreBaseURL: mockServerURL, 476 } 477 dauthCtx := &testDauthContext{c: c, device: s.device} 478 sto := store.New(&cfg, dauthCtx) 479 480 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 481 { 482 InstanceName: "hello-world", 483 SnapID: helloWorldSnapID, 484 TrackingChannel: "stable", 485 Revision: snap.R(1), 486 RefreshedDate: helloRefreshedDate, 487 Block: []snap.Revision{snap.R(26)}, 488 }, 489 }, []*store.SnapAction{ 490 { 491 Action: "refresh", 492 SnapID: helloWorldSnapID, 493 InstanceName: "hello-world", 494 Channel: "stable", 495 }, 496 }, nil, nil, nil) 497 c.Assert(results, HasLen, 0) 498 c.Check(err, DeepEquals, &store.SnapActionError{ 499 Refresh: map[string]error{ 500 "hello-world": store.ErrNoUpdateAvailable, 501 }, 502 }) 503 } 504 505 func (s *storeActionSuite) TestSnapActionSkipCurrent(c *C) { 506 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 507 assertRequest(c, r, "POST", snapActionPath) 508 // check device authorization is set, implicitly checking doRequest was used 509 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 510 511 jsonReq, err := ioutil.ReadAll(r.Body) 512 c.Assert(err, IsNil) 513 var req struct { 514 Context []map[string]interface{} `json:"context"` 515 Actions []map[string]interface{} `json:"actions"` 516 } 517 518 err = json.Unmarshal(jsonReq, &req) 519 c.Assert(err, IsNil) 520 521 c.Assert(req.Context, HasLen, 1) 522 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 523 "snap-id": helloWorldSnapID, 524 "instance-key": helloWorldSnapID, 525 "revision": float64(26), 526 "tracking-channel": "stable", 527 "refreshed-date": helloRefreshedDateStr, 528 "epoch": iZeroEpoch, 529 }) 530 c.Assert(req.Actions, HasLen, 1) 531 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 532 "action": "refresh", 533 "instance-key": helloWorldSnapID, 534 "snap-id": helloWorldSnapID, 535 "channel": "stable", 536 }) 537 538 io.WriteString(w, `{ 539 "results": [{ 540 "result": "refresh", 541 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 542 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 543 "name": "hello-world", 544 "snap": { 545 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 546 "name": "hello-world", 547 "revision": 26, 548 "version": "6.1", 549 "publisher": { 550 "id": "canonical", 551 "username": "canonical", 552 "display-name": "Canonical" 553 } 554 } 555 }] 556 }`) 557 })) 558 559 c.Assert(mockServer, NotNil) 560 defer mockServer.Close() 561 562 mockServerURL, _ := url.Parse(mockServer.URL) 563 cfg := store.Config{ 564 StoreBaseURL: mockServerURL, 565 } 566 dauthCtx := &testDauthContext{c: c, device: s.device} 567 sto := store.New(&cfg, dauthCtx) 568 569 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 570 { 571 InstanceName: "hello-world", 572 SnapID: helloWorldSnapID, 573 TrackingChannel: "stable", 574 Revision: snap.R(26), 575 RefreshedDate: helloRefreshedDate, 576 }, 577 }, []*store.SnapAction{ 578 { 579 Action: "refresh", 580 SnapID: helloWorldSnapID, 581 InstanceName: "hello-world", 582 Channel: "stable", 583 }, 584 }, nil, nil, nil) 585 c.Assert(results, HasLen, 0) 586 c.Check(err, DeepEquals, &store.SnapActionError{ 587 Refresh: map[string]error{ 588 "hello-world": store.ErrNoUpdateAvailable, 589 }, 590 }) 591 } 592 593 func (s *storeActionSuite) TestSnapActionRetryOnEOF(c *C) { 594 n := 0 595 var mockServer *httptest.Server 596 mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 597 assertRequest(c, r, "POST", snapActionPath) 598 n++ 599 if n < 4 { 600 io.WriteString(w, "{") 601 mockServer.CloseClientConnections() 602 return 603 } 604 605 var req struct { 606 Context []map[string]interface{} `json:"context"` 607 Actions []map[string]interface{} `json:"actions"` 608 } 609 610 err := json.NewDecoder(r.Body).Decode(&req) 611 c.Assert(err, IsNil) 612 c.Assert(req.Context, HasLen, 1) 613 c.Assert(req.Actions, HasLen, 1) 614 io.WriteString(w, `{ 615 "results": [{ 616 "result": "refresh", 617 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 618 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 619 "name": "hello-world", 620 "snap": { 621 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 622 "name": "hello-world", 623 "revision": 26, 624 "version": "6.1", 625 "publisher": { 626 "id": "canonical", 627 "username": "canonical", 628 "display-name": "Canonical" 629 } 630 } 631 }] 632 }`) 633 })) 634 635 c.Assert(mockServer, NotNil) 636 defer mockServer.Close() 637 638 mockServerURL, _ := url.Parse(mockServer.URL) 639 cfg := store.Config{ 640 StoreBaseURL: mockServerURL, 641 } 642 dauthCtx := &testDauthContext{c: c, device: s.device} 643 sto := store.New(&cfg, dauthCtx) 644 645 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 646 { 647 InstanceName: "hello-world", 648 SnapID: helloWorldSnapID, 649 TrackingChannel: "stable", 650 Revision: snap.R(1), 651 }, 652 }, []*store.SnapAction{ 653 { 654 Action: "refresh", 655 SnapID: helloWorldSnapID, 656 InstanceName: "hello-world", 657 Channel: "stable", 658 }, 659 }, nil, nil, nil) 660 c.Assert(err, IsNil) 661 c.Assert(n, Equals, 4) 662 c.Assert(results, HasLen, 1) 663 c.Assert(results[0].InstanceName(), Equals, "hello-world") 664 } 665 666 func (s *storeActionSuite) TestSnapActionIgnoreValidation(c *C) { 667 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 668 assertRequest(c, r, "POST", snapActionPath) 669 // check device authorization is set, implicitly checking doRequest was used 670 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 671 672 jsonReq, err := ioutil.ReadAll(r.Body) 673 c.Assert(err, IsNil) 674 var req struct { 675 Context []map[string]interface{} `json:"context"` 676 Actions []map[string]interface{} `json:"actions"` 677 } 678 679 err = json.Unmarshal(jsonReq, &req) 680 c.Assert(err, IsNil) 681 682 c.Assert(req.Context, HasLen, 1) 683 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 684 "snap-id": helloWorldSnapID, 685 "instance-key": helloWorldSnapID, 686 "revision": float64(1), 687 "tracking-channel": "stable", 688 "refreshed-date": helloRefreshedDateStr, 689 "ignore-validation": true, 690 "epoch": iZeroEpoch, 691 }) 692 c.Assert(req.Actions, HasLen, 1) 693 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 694 "action": "refresh", 695 "instance-key": helloWorldSnapID, 696 "snap-id": helloWorldSnapID, 697 "channel": "stable", 698 "ignore-validation": false, 699 }) 700 701 io.WriteString(w, `{ 702 "results": [{ 703 "result": "refresh", 704 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 705 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 706 "name": "hello-world", 707 "snap": { 708 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 709 "name": "hello-world", 710 "revision": 26, 711 "version": "6.1", 712 "publisher": { 713 "id": "canonical", 714 "username": "canonical", 715 "display-name": "Canonical" 716 } 717 } 718 }] 719 }`) 720 })) 721 722 c.Assert(mockServer, NotNil) 723 defer mockServer.Close() 724 725 mockServerURL, _ := url.Parse(mockServer.URL) 726 cfg := store.Config{ 727 StoreBaseURL: mockServerURL, 728 } 729 dauthCtx := &testDauthContext{c: c, device: s.device} 730 sto := store.New(&cfg, dauthCtx) 731 732 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 733 { 734 InstanceName: "hello-world", 735 SnapID: helloWorldSnapID, 736 TrackingChannel: "stable", 737 Revision: snap.R(1), 738 RefreshedDate: helloRefreshedDate, 739 IgnoreValidation: true, 740 }, 741 }, []*store.SnapAction{ 742 { 743 Action: "refresh", 744 SnapID: helloWorldSnapID, 745 InstanceName: "hello-world", 746 Channel: "stable", 747 Flags: store.SnapActionEnforceValidation, 748 }, 749 }, nil, nil, nil) 750 c.Assert(err, IsNil) 751 c.Assert(results, HasLen, 1) 752 c.Assert(results[0].InstanceName(), Equals, "hello-world") 753 c.Assert(results[0].Revision, Equals, snap.R(26)) 754 } 755 756 func (s *storeActionSuite) TestSnapActionInstallWithValidationSets(c *C) { 757 s.testSnapActionGet("install", "", "", [][]string{{"foo", "bar"}, {"foo", "baz"}}, c) 758 } 759 760 func (s *storeActionSuite) TestSnapActionAutoRefresh(c *C) { 761 // the bare TestSnapAction does more SnapAction checks; look there 762 // this one mostly just checks the refresh-reason header 763 764 restore := release.MockOnClassic(false) 765 defer restore() 766 767 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 768 assertRequest(c, r, "POST", snapActionPath) 769 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled") 770 771 io.WriteString(w, `{ 772 "results": [{ 773 "result": "refresh", 774 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 775 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 776 "name": "hello-world", 777 "snap": { 778 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 779 "name": "hello-world", 780 "revision": 26, 781 "version": "6.1", 782 "epoch": {"read": [0], "write": [0]}, 783 "publisher": { 784 "id": "canonical", 785 "username": "canonical", 786 "display-name": "Canonical" 787 } 788 } 789 }] 790 }`) 791 })) 792 793 c.Assert(mockServer, NotNil) 794 defer mockServer.Close() 795 796 mockServerURL, _ := url.Parse(mockServer.URL) 797 cfg := store.Config{ 798 StoreBaseURL: mockServerURL, 799 } 800 dauthCtx := &testDauthContext{c: c, device: s.device} 801 sto := store.New(&cfg, dauthCtx) 802 803 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 804 { 805 InstanceName: "hello-world", 806 SnapID: helloWorldSnapID, 807 TrackingChannel: "beta", 808 Revision: snap.R(1), 809 RefreshedDate: helloRefreshedDate, 810 }, 811 }, []*store.SnapAction{ 812 { 813 Action: "refresh", 814 SnapID: helloWorldSnapID, 815 InstanceName: "hello-world", 816 }, 817 }, nil, nil, &store.RefreshOptions{IsAutoRefresh: true}) 818 c.Assert(err, IsNil) 819 c.Assert(results, HasLen, 1) 820 } 821 822 func (s *storeActionSuite) TestInstallFallbackChannelIsStable(c *C) { 823 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 824 assertRequest(c, r, "POST", snapActionPath) 825 // check device authorization is set, implicitly checking doRequest was used 826 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 827 828 jsonReq, err := ioutil.ReadAll(r.Body) 829 c.Assert(err, IsNil) 830 var req struct { 831 Context []map[string]interface{} `json:"context"` 832 Actions []map[string]interface{} `json:"actions"` 833 } 834 835 err = json.Unmarshal(jsonReq, &req) 836 c.Assert(err, IsNil) 837 838 c.Assert(req.Context, HasLen, 1) 839 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 840 "snap-id": helloWorldSnapID, 841 "instance-key": helloWorldSnapID, 842 "revision": float64(1), 843 "tracking-channel": "stable", 844 "refreshed-date": helloRefreshedDateStr, 845 "epoch": iZeroEpoch, 846 }) 847 c.Assert(req.Actions, HasLen, 1) 848 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 849 "action": "refresh", 850 "instance-key": helloWorldSnapID, 851 "snap-id": helloWorldSnapID, 852 }) 853 854 io.WriteString(w, `{ 855 "results": [{ 856 "result": "refresh", 857 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 858 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 859 "name": "hello-world", 860 "snap": { 861 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 862 "name": "hello-world", 863 "revision": 26, 864 "version": "6.1", 865 "publisher": { 866 "id": "canonical", 867 "username": "canonical", 868 "display-name": "Canonical" 869 } 870 } 871 }] 872 }`) 873 })) 874 875 c.Assert(mockServer, NotNil) 876 defer mockServer.Close() 877 878 mockServerURL, _ := url.Parse(mockServer.URL) 879 cfg := store.Config{ 880 StoreBaseURL: mockServerURL, 881 } 882 dauthCtx := &testDauthContext{c: c, device: s.device} 883 sto := store.New(&cfg, dauthCtx) 884 885 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 886 { 887 InstanceName: "hello-world", 888 SnapID: helloWorldSnapID, 889 RefreshedDate: helloRefreshedDate, 890 Revision: snap.R(1), 891 }, 892 }, []*store.SnapAction{ 893 { 894 Action: "refresh", 895 SnapID: helloWorldSnapID, 896 InstanceName: "hello-world", 897 }, 898 }, nil, nil, nil) 899 c.Assert(err, IsNil) 900 c.Assert(results, HasLen, 1) 901 c.Assert(results[0].InstanceName(), Equals, "hello-world") 902 c.Assert(results[0].Revision, Equals, snap.R(26)) 903 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 904 } 905 906 func (s *storeActionSuite) TestSnapActionNonDefaultsHeaders(c *C) { 907 restore := release.MockOnClassic(true) 908 defer restore() 909 910 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 911 assertRequest(c, r, "POST", snapActionPath) 912 // check device authorization is set, implicitly checking doRequest was used 913 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 914 915 storeID := r.Header.Get("Snap-Device-Store") 916 c.Check(storeID, Equals, "foo") 917 918 c.Check(r.Header.Get("Snap-Device-Series"), Equals, "21") 919 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, "archXYZ") 920 c.Check(r.Header.Get("Snap-Classic"), Equals, "true") 921 922 jsonReq, err := ioutil.ReadAll(r.Body) 923 c.Assert(err, IsNil) 924 var req struct { 925 Context []map[string]interface{} `json:"context"` 926 Actions []map[string]interface{} `json:"actions"` 927 } 928 929 err = json.Unmarshal(jsonReq, &req) 930 c.Assert(err, IsNil) 931 932 c.Assert(req.Context, HasLen, 1) 933 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 934 "snap-id": helloWorldSnapID, 935 "instance-key": helloWorldSnapID, 936 "revision": float64(1), 937 "tracking-channel": "beta", 938 "refreshed-date": helloRefreshedDateStr, 939 "epoch": iZeroEpoch, 940 }) 941 c.Assert(req.Actions, HasLen, 1) 942 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 943 "action": "refresh", 944 "instance-key": helloWorldSnapID, 945 "snap-id": helloWorldSnapID, 946 }) 947 948 io.WriteString(w, `{ 949 "results": [{ 950 "result": "refresh", 951 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 952 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 953 "name": "hello-world", 954 "snap": { 955 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 956 "name": "hello-world", 957 "revision": 26, 958 "version": "6.1", 959 "publisher": { 960 "id": "canonical", 961 "username": "canonical", 962 "display-name": "Canonical" 963 } 964 } 965 }] 966 }`) 967 })) 968 969 c.Assert(mockServer, NotNil) 970 defer mockServer.Close() 971 972 mockServerURL, _ := url.Parse(mockServer.URL) 973 cfg := store.DefaultConfig() 974 cfg.StoreBaseURL = mockServerURL 975 cfg.Series = "21" 976 cfg.Architecture = "archXYZ" 977 cfg.StoreID = "foo" 978 dauthCtx := &testDauthContext{c: c, device: s.device} 979 sto := store.New(cfg, dauthCtx) 980 981 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 982 { 983 InstanceName: "hello-world", 984 SnapID: helloWorldSnapID, 985 TrackingChannel: "beta", 986 RefreshedDate: helloRefreshedDate, 987 Revision: snap.R(1), 988 }, 989 }, []*store.SnapAction{ 990 { 991 Action: "refresh", 992 SnapID: helloWorldSnapID, 993 InstanceName: "hello-world", 994 }, 995 }, nil, nil, nil) 996 c.Assert(err, IsNil) 997 c.Assert(results, HasLen, 1) 998 c.Assert(results[0].InstanceName(), Equals, "hello-world") 999 c.Assert(results[0].Revision, Equals, snap.R(26)) 1000 c.Assert(results[0].Version, Equals, "6.1") 1001 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1002 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1003 c.Assert(results[0].Deltas, HasLen, 0) 1004 } 1005 1006 func (s *storeActionSuite) TestSnapActionWithDeltas(c *C) { 1007 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 1008 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 1009 c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil) 1010 1011 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1012 assertRequest(c, r, "POST", snapActionPath) 1013 // check device authorization is set, implicitly checking doRequest was used 1014 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1015 1016 c.Check(r.Header.Get("Snap-Accept-Delta-Format"), Equals, "xdelta3") 1017 jsonReq, err := ioutil.ReadAll(r.Body) 1018 c.Assert(err, IsNil) 1019 var req struct { 1020 Context []map[string]interface{} `json:"context"` 1021 Actions []map[string]interface{} `json:"actions"` 1022 } 1023 1024 err = json.Unmarshal(jsonReq, &req) 1025 c.Assert(err, IsNil) 1026 1027 c.Assert(req.Context, HasLen, 1) 1028 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1029 "snap-id": helloWorldSnapID, 1030 "instance-key": helloWorldSnapID, 1031 "revision": float64(1), 1032 "tracking-channel": "beta", 1033 "refreshed-date": helloRefreshedDateStr, 1034 "epoch": iZeroEpoch, 1035 }) 1036 c.Assert(req.Actions, HasLen, 1) 1037 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1038 "action": "refresh", 1039 "instance-key": helloWorldSnapID, 1040 "snap-id": helloWorldSnapID, 1041 }) 1042 1043 io.WriteString(w, `{ 1044 "results": [{ 1045 "result": "refresh", 1046 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1047 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1048 "name": "hello-world", 1049 "snap": { 1050 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1051 "name": "hello-world", 1052 "revision": 26, 1053 "version": "6.1", 1054 "publisher": { 1055 "id": "canonical", 1056 "username": "canonical", 1057 "display-name": "Canonical" 1058 } 1059 } 1060 }] 1061 }`) 1062 })) 1063 1064 c.Assert(mockServer, NotNil) 1065 defer mockServer.Close() 1066 1067 mockServerURL, _ := url.Parse(mockServer.URL) 1068 cfg := store.Config{ 1069 StoreBaseURL: mockServerURL, 1070 } 1071 dauthCtx := &testDauthContext{c: c, device: s.device} 1072 sto := store.New(&cfg, dauthCtx) 1073 1074 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1075 { 1076 InstanceName: "hello-world", 1077 SnapID: helloWorldSnapID, 1078 TrackingChannel: "beta", 1079 Revision: snap.R(1), 1080 RefreshedDate: helloRefreshedDate, 1081 }, 1082 }, []*store.SnapAction{ 1083 { 1084 Action: "refresh", 1085 SnapID: helloWorldSnapID, 1086 InstanceName: "hello-world", 1087 }, 1088 }, nil, nil, nil) 1089 c.Assert(err, IsNil) 1090 c.Assert(results, HasLen, 1) 1091 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1092 c.Assert(results[0].Revision, Equals, snap.R(26)) 1093 } 1094 1095 func (s *storeActionSuite) TestSnapActionOptions(c *C) { 1096 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1097 assertRequest(c, r, "POST", snapActionPath) 1098 // check device authorization is set, implicitly checking doRequest was used 1099 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1100 1101 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "true") 1102 1103 jsonReq, err := ioutil.ReadAll(r.Body) 1104 c.Assert(err, IsNil) 1105 var req struct { 1106 Context []map[string]interface{} `json:"context"` 1107 Actions []map[string]interface{} `json:"actions"` 1108 } 1109 1110 err = json.Unmarshal(jsonReq, &req) 1111 c.Assert(err, IsNil) 1112 1113 c.Assert(req.Context, HasLen, 1) 1114 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1115 "snap-id": helloWorldSnapID, 1116 "instance-key": helloWorldSnapID, 1117 "revision": float64(1), 1118 "tracking-channel": "stable", 1119 "refreshed-date": helloRefreshedDateStr, 1120 "epoch": iZeroEpoch, 1121 }) 1122 c.Assert(req.Actions, HasLen, 1) 1123 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1124 "action": "refresh", 1125 "instance-key": helloWorldSnapID, 1126 "snap-id": helloWorldSnapID, 1127 "channel": "stable", 1128 }) 1129 1130 io.WriteString(w, `{ 1131 "results": [{ 1132 "result": "refresh", 1133 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1134 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1135 "name": "hello-world", 1136 "snap": { 1137 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1138 "name": "hello-world", 1139 "revision": 26, 1140 "version": "6.1", 1141 "publisher": { 1142 "id": "canonical", 1143 "username": "canonical", 1144 "display-name": "Canonical" 1145 } 1146 } 1147 }] 1148 }`) 1149 })) 1150 1151 c.Assert(mockServer, NotNil) 1152 defer mockServer.Close() 1153 1154 mockServerURL, _ := url.Parse(mockServer.URL) 1155 cfg := store.Config{ 1156 StoreBaseURL: mockServerURL, 1157 } 1158 dauthCtx := &testDauthContext{c: c, device: s.device} 1159 sto := store.New(&cfg, dauthCtx) 1160 1161 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1162 { 1163 InstanceName: "hello-world", 1164 SnapID: helloWorldSnapID, 1165 TrackingChannel: "stable", 1166 Revision: snap.R(1), 1167 RefreshedDate: helloRefreshedDate, 1168 }, 1169 }, []*store.SnapAction{ 1170 { 1171 Action: "refresh", 1172 SnapID: helloWorldSnapID, 1173 InstanceName: "hello-world", 1174 Channel: "stable", 1175 }, 1176 }, nil, nil, &store.RefreshOptions{RefreshManaged: true}) 1177 c.Assert(err, IsNil) 1178 c.Assert(results, HasLen, 1) 1179 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1180 c.Assert(results[0].Revision, Equals, snap.R(26)) 1181 } 1182 1183 func (s *storeActionSuite) TestSnapActionInstall(c *C) { 1184 s.testSnapActionGet("install", "", "", nil, c) 1185 } 1186 func (s *storeActionSuite) TestSnapActionInstallWithCohort(c *C) { 1187 s.testSnapActionGet("install", "what", "", nil, c) 1188 } 1189 func (s *storeActionSuite) TestSnapActionDownload(c *C) { 1190 s.testSnapActionGet("download", "", "", nil, c) 1191 } 1192 func (s *storeActionSuite) TestSnapActionDownloadWithCohort(c *C) { 1193 s.testSnapActionGet("download", "here", "", nil, c) 1194 } 1195 func (s *storeActionSuite) TestSnapActionInstallRedirect(c *C) { 1196 s.testSnapActionGet("install", "", "2.0/candidate", nil, c) 1197 } 1198 func (s *storeActionSuite) TestSnapActionDownloadRedirect(c *C) { 1199 s.testSnapActionGet("download", "", "2.0/candidate", nil, c) 1200 } 1201 func (s *storeActionSuite) testSnapActionGet(action, cohort, redirectChannel string, validationSets [][]string, c *C) { 1202 // action here is one of install or download 1203 restore := release.MockOnClassic(false) 1204 defer restore() 1205 1206 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1207 assertRequest(c, r, "POST", snapActionPath) 1208 // check device authorization is set, implicitly checking doRequest was used 1209 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1210 1211 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 1212 1213 // no store ID by default 1214 storeID := r.Header.Get("Snap-Device-Store") 1215 c.Check(storeID, Equals, "") 1216 1217 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 1218 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 1219 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 1220 1221 jsonReq, err := ioutil.ReadAll(r.Body) 1222 c.Assert(err, IsNil) 1223 var req struct { 1224 Context []map[string]interface{} `json:"context"` 1225 Actions []map[string]interface{} `json:"actions"` 1226 } 1227 1228 err = json.Unmarshal(jsonReq, &req) 1229 c.Assert(err, IsNil) 1230 1231 c.Assert(req.Context, HasLen, 0) 1232 c.Assert(req.Actions, HasLen, 1) 1233 expectedAction := map[string]interface{}{ 1234 "action": action, 1235 "instance-key": action + "-1", 1236 "name": "hello-world", 1237 "channel": "beta", 1238 "epoch": nil, 1239 } 1240 if cohort != "" { 1241 expectedAction["cohort-key"] = cohort 1242 } 1243 if validationSets != nil { 1244 // XXX: rewrite as otherwise DeepEquals complains about 1245 // []interface {}{[]interface {}{..} vs expected [][]string{[]string{..}. 1246 var sets []interface{} 1247 for _, vs := range validationSets { 1248 var vss []interface{} 1249 for _, vv := range vs { 1250 vss = append(vss, vv) 1251 } 1252 sets = append(sets, vss) 1253 } 1254 expectedAction["validation-sets"] = sets 1255 } 1256 c.Assert(req.Actions[0], DeepEquals, expectedAction) 1257 1258 fmt.Fprintf(w, `{ 1259 "results": [{ 1260 "result": "%s", 1261 "instance-key": "%[1]s-1", 1262 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1263 "name": "hello-world", 1264 "effective-channel": "candidate", 1265 "redirect-channel": "%s", 1266 "snap": { 1267 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1268 "name": "hello-world", 1269 "revision": 26, 1270 "version": "6.1", 1271 "publisher": { 1272 "id": "canonical", 1273 "username": "canonical", 1274 "display-name": "Canonical" 1275 } 1276 } 1277 }] 1278 }`, action, redirectChannel) 1279 })) 1280 1281 c.Assert(mockServer, NotNil) 1282 defer mockServer.Close() 1283 1284 mockServerURL, _ := url.Parse(mockServer.URL) 1285 cfg := store.Config{ 1286 StoreBaseURL: mockServerURL, 1287 } 1288 dauthCtx := &testDauthContext{c: c, device: s.device} 1289 sto := store.New(&cfg, dauthCtx) 1290 1291 results, _, err := sto.SnapAction(s.ctx, nil, 1292 []*store.SnapAction{ 1293 { 1294 Action: action, 1295 InstanceName: "hello-world", 1296 Channel: "beta", 1297 CohortKey: cohort, 1298 ValidationSets: validationSets, 1299 }, 1300 }, nil, nil, nil) 1301 c.Assert(err, IsNil) 1302 c.Assert(results, HasLen, 1) 1303 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1304 c.Assert(results[0].Revision, Equals, snap.R(26)) 1305 c.Assert(results[0].Version, Equals, "6.1") 1306 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1307 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1308 c.Assert(results[0].Deltas, HasLen, 0) 1309 // effective-channel 1310 c.Assert(results[0].Channel, Equals, "candidate") 1311 c.Assert(results[0].RedirectChannel, Equals, redirectChannel) 1312 } 1313 1314 func (s *storeActionSuite) TestSnapActionInstallAmend(c *C) { 1315 // this is what amend would look like 1316 restore := release.MockOnClassic(false) 1317 defer restore() 1318 1319 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1320 assertRequest(c, r, "POST", snapActionPath) 1321 // check device authorization is set, implicitly checking doRequest was used 1322 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1323 1324 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 1325 1326 // no store ID by default 1327 storeID := r.Header.Get("Snap-Device-Store") 1328 c.Check(storeID, Equals, "") 1329 1330 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 1331 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 1332 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 1333 1334 jsonReq, err := ioutil.ReadAll(r.Body) 1335 c.Assert(err, IsNil) 1336 var req struct { 1337 Context []map[string]interface{} `json:"context"` 1338 Actions []map[string]interface{} `json:"actions"` 1339 } 1340 1341 err = json.Unmarshal(jsonReq, &req) 1342 c.Assert(err, IsNil) 1343 1344 c.Assert(req.Context, HasLen, 0) 1345 c.Assert(req.Actions, HasLen, 1) 1346 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1347 "action": "install", 1348 "instance-key": "install-1", 1349 "name": "hello-world", 1350 "channel": "beta", 1351 "epoch": map[string]interface{}{"read": []interface{}{0., 1.}, "write": []interface{}{1.}}, 1352 }) 1353 1354 fmt.Fprint(w, `{ 1355 "results": [{ 1356 "result": "install", 1357 "instance-key": "install-1", 1358 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1359 "name": "hello-world", 1360 "effective-channel": "candidate", 1361 "snap": { 1362 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1363 "name": "hello-world", 1364 "revision": 26, 1365 "version": "6.1", 1366 "publisher": { 1367 "id": "canonical", 1368 "username": "canonical", 1369 "display-name": "Canonical" 1370 } 1371 } 1372 }] 1373 }`) 1374 })) 1375 1376 c.Assert(mockServer, NotNil) 1377 defer mockServer.Close() 1378 1379 mockServerURL, _ := url.Parse(mockServer.URL) 1380 cfg := store.Config{ 1381 StoreBaseURL: mockServerURL, 1382 } 1383 dauthCtx := &testDauthContext{c: c, device: s.device} 1384 sto := store.New(&cfg, dauthCtx) 1385 1386 results, _, err := sto.SnapAction(s.ctx, nil, 1387 []*store.SnapAction{ 1388 { 1389 Action: "install", 1390 InstanceName: "hello-world", 1391 Channel: "beta", 1392 Epoch: snap.E("1*"), 1393 }, 1394 }, nil, nil, nil) 1395 c.Assert(err, IsNil) 1396 c.Assert(results, HasLen, 1) 1397 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1398 c.Assert(results[0].Revision, Equals, snap.R(26)) 1399 c.Assert(results[0].Version, Equals, "6.1") 1400 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1401 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1402 c.Assert(results[0].Deltas, HasLen, 0) 1403 // effective-channel 1404 c.Assert(results[0].Channel, Equals, "candidate") 1405 } 1406 1407 func (s *storeActionSuite) TestSnapActionWithClientUserAgent(c *C) { 1408 restore := release.MockOnClassic(false) 1409 defer restore() 1410 1411 serverCalls := 0 1412 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1413 serverCalls++ 1414 assertRequest(c, r, "POST", snapActionPath) 1415 1416 c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, "some-snap-agent/1.0") 1417 1418 io.WriteString(w, `{ 1419 "results": [] 1420 }`) 1421 })) 1422 1423 c.Assert(mockServer, NotNil) 1424 defer mockServer.Close() 1425 1426 mockServerURL, _ := url.Parse(mockServer.URL) 1427 cfg := store.Config{ 1428 StoreBaseURL: mockServerURL, 1429 } 1430 dauthCtx := &testDauthContext{c: c, device: s.device} 1431 sto := store.New(&cfg, dauthCtx) 1432 1433 // to construct the client-user-agent context we need to 1434 // create a req that simulates what the req that the daemon got 1435 r, err := http.NewRequest("POST", "/snapd/api", nil) 1436 r.Header.Set("User-Agent", "some-snap-agent/1.0") 1437 c.Assert(err, IsNil) 1438 ctx := store.WithClientUserAgent(s.ctx, r) 1439 1440 results, _, err := sto.SnapAction(ctx, nil, []*store.SnapAction{{Action: "install", InstanceName: "some-snap"}}, nil, nil, nil) 1441 c.Check(serverCalls, Equals, 1) 1442 c.Check(results, HasLen, 0) 1443 c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true}) 1444 } 1445 1446 func (s *storeActionSuite) TestSnapActionDownloadParallelInstanceKey(c *C) { 1447 // action here is one of install or download 1448 restore := release.MockOnClassic(false) 1449 defer restore() 1450 1451 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1452 c.Fatal("should not be reached") 1453 })) 1454 1455 c.Assert(mockServer, NotNil) 1456 defer mockServer.Close() 1457 1458 mockServerURL, _ := url.Parse(mockServer.URL) 1459 cfg := store.Config{ 1460 StoreBaseURL: mockServerURL, 1461 } 1462 dauthCtx := &testDauthContext{c: c, device: s.device} 1463 sto := store.New(&cfg, dauthCtx) 1464 1465 _, _, err := sto.SnapAction(s.ctx, nil, 1466 []*store.SnapAction{ 1467 { 1468 Action: "download", 1469 InstanceName: "hello-world_foo", 1470 Channel: "beta", 1471 }, 1472 }, nil, nil, nil) 1473 c.Assert(err, ErrorMatches, `internal error: unsupported download with instance name "hello-world_foo"`) 1474 } 1475 1476 func (s *storeActionSuite) TestSnapActionInstallWithRevision(c *C) { 1477 s.testSnapActionGetWithRevision("install", c) 1478 } 1479 1480 func (s *storeActionSuite) TestSnapActionDownloadWithRevision(c *C) { 1481 s.testSnapActionGetWithRevision("download", c) 1482 } 1483 1484 func (s *storeActionSuite) testSnapActionGetWithRevision(action string, c *C) { 1485 // action here is one of install or download 1486 restore := release.MockOnClassic(false) 1487 defer restore() 1488 1489 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1490 assertRequest(c, r, "POST", snapActionPath) 1491 // check device authorization is set, implicitly checking doRequest was used 1492 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1493 1494 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 1495 1496 // no store ID by default 1497 storeID := r.Header.Get("Snap-Device-Store") 1498 c.Check(storeID, Equals, "") 1499 1500 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 1501 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 1502 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 1503 1504 jsonReq, err := ioutil.ReadAll(r.Body) 1505 c.Assert(err, IsNil) 1506 var req struct { 1507 Context []map[string]interface{} `json:"context"` 1508 Actions []map[string]interface{} `json:"actions"` 1509 } 1510 1511 err = json.Unmarshal(jsonReq, &req) 1512 c.Assert(err, IsNil) 1513 1514 c.Assert(req.Context, HasLen, 0) 1515 c.Assert(req.Actions, HasLen, 1) 1516 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1517 "action": action, 1518 "instance-key": action + "-1", 1519 "name": "hello-world", 1520 "revision": float64(28), 1521 "epoch": nil, 1522 }) 1523 1524 fmt.Fprintf(w, `{ 1525 "results": [{ 1526 "result": "%s", 1527 "instance-key": "%[1]s-1", 1528 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1529 "name": "hello-world", 1530 "snap": { 1531 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1532 "name": "hello-world", 1533 "revision": 28, 1534 "version": "6.1", 1535 "publisher": { 1536 "id": "canonical", 1537 "username": "canonical", 1538 "display-name": "Canonical" 1539 } 1540 } 1541 }] 1542 }`, action) 1543 })) 1544 1545 c.Assert(mockServer, NotNil) 1546 defer mockServer.Close() 1547 1548 mockServerURL, _ := url.Parse(mockServer.URL) 1549 cfg := store.Config{ 1550 StoreBaseURL: mockServerURL, 1551 } 1552 dauthCtx := &testDauthContext{c: c, device: s.device} 1553 sto := store.New(&cfg, dauthCtx) 1554 1555 results, _, err := sto.SnapAction(s.ctx, nil, 1556 []*store.SnapAction{ 1557 { 1558 Action: action, 1559 InstanceName: "hello-world", 1560 Revision: snap.R(28), 1561 }, 1562 }, nil, nil, nil) 1563 c.Assert(err, IsNil) 1564 c.Assert(results, HasLen, 1) 1565 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1566 c.Assert(results[0].Revision, Equals, snap.R(28)) 1567 c.Assert(results[0].Version, Equals, "6.1") 1568 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1569 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1570 c.Assert(results[0].Deltas, HasLen, 0) 1571 // effective-channel is not set 1572 c.Assert(results[0].Channel, Equals, "") 1573 } 1574 1575 func (s *storeActionSuite) TestSnapActionRevisionNotAvailable(c *C) { 1576 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1577 assertRequest(c, r, "POST", snapActionPath) 1578 // check device authorization is set, implicitly checking doRequest was used 1579 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1580 1581 jsonReq, err := ioutil.ReadAll(r.Body) 1582 c.Assert(err, IsNil) 1583 var req struct { 1584 Context []map[string]interface{} `json:"context"` 1585 Actions []map[string]interface{} `json:"actions"` 1586 } 1587 1588 err = json.Unmarshal(jsonReq, &req) 1589 c.Assert(err, IsNil) 1590 1591 c.Assert(req.Context, HasLen, 2) 1592 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1593 "snap-id": helloWorldSnapID, 1594 "instance-key": helloWorldSnapID, 1595 "revision": float64(26), 1596 "tracking-channel": "stable", 1597 "refreshed-date": helloRefreshedDateStr, 1598 "epoch": iZeroEpoch, 1599 }) 1600 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 1601 "snap-id": "snap2-id", 1602 "instance-key": "snap2-id", 1603 "revision": float64(2), 1604 "tracking-channel": "edge", 1605 "refreshed-date": helloRefreshedDateStr, 1606 "epoch": iZeroEpoch, 1607 }) 1608 c.Assert(req.Actions, HasLen, 4) 1609 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1610 "action": "refresh", 1611 "instance-key": helloWorldSnapID, 1612 "snap-id": helloWorldSnapID, 1613 }) 1614 c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ 1615 "action": "refresh", 1616 "instance-key": "snap2-id", 1617 "snap-id": "snap2-id", 1618 "channel": "candidate", 1619 }) 1620 c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ 1621 "action": "install", 1622 "instance-key": "install-1", 1623 "name": "foo", 1624 "channel": "stable", 1625 "epoch": nil, 1626 }) 1627 c.Assert(req.Actions[3], DeepEquals, map[string]interface{}{ 1628 "action": "download", 1629 "instance-key": "download-1", 1630 "name": "bar", 1631 "revision": 42., 1632 "epoch": nil, 1633 }) 1634 1635 io.WriteString(w, `{ 1636 "results": [{ 1637 "result": "error", 1638 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1639 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1640 "name": "hello-world", 1641 "error": { 1642 "code": "revision-not-found", 1643 "message": "msg1" 1644 } 1645 }, { 1646 "result": "error", 1647 "instance-key": "snap2-id", 1648 "snap-id": "snap2-id", 1649 "name": "snap2", 1650 "error": { 1651 "code": "revision-not-found", 1652 "message": "msg1", 1653 "extra": { 1654 "releases": [{"architecture": "amd64", "channel": "beta"}, 1655 {"architecture": "arm64", "channel": "beta"}] 1656 } 1657 } 1658 }, { 1659 "result": "error", 1660 "instance-key": "install-1", 1661 "snap-id": "foo-id", 1662 "name": "foo", 1663 "error": { 1664 "code": "revision-not-found", 1665 "message": "msg2" 1666 } 1667 }, { 1668 "result": "error", 1669 "instance-key": "download-1", 1670 "snap-id": "bar-id", 1671 "name": "bar", 1672 "error": { 1673 "code": "revision-not-found", 1674 "message": "msg3" 1675 } 1676 }] 1677 }`) 1678 })) 1679 1680 c.Assert(mockServer, NotNil) 1681 defer mockServer.Close() 1682 1683 mockServerURL, _ := url.Parse(mockServer.URL) 1684 cfg := store.Config{ 1685 StoreBaseURL: mockServerURL, 1686 } 1687 dauthCtx := &testDauthContext{c: c, device: s.device} 1688 sto := store.New(&cfg, dauthCtx) 1689 1690 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1691 { 1692 InstanceName: "hello-world", 1693 SnapID: helloWorldSnapID, 1694 TrackingChannel: "stable", 1695 Revision: snap.R(26), 1696 RefreshedDate: helloRefreshedDate, 1697 }, 1698 { 1699 InstanceName: "snap2", 1700 SnapID: "snap2-id", 1701 TrackingChannel: "edge", 1702 Revision: snap.R(2), 1703 RefreshedDate: helloRefreshedDate, 1704 }, 1705 }, []*store.SnapAction{ 1706 { 1707 Action: "refresh", 1708 InstanceName: "hello-world", 1709 SnapID: helloWorldSnapID, 1710 }, { 1711 Action: "refresh", 1712 InstanceName: "snap2", 1713 SnapID: "snap2-id", 1714 Channel: "candidate", 1715 }, { 1716 Action: "install", 1717 InstanceName: "foo", 1718 Channel: "stable", 1719 }, { 1720 Action: "download", 1721 InstanceName: "bar", 1722 Revision: snap.R(42), 1723 }, 1724 }, nil, nil, nil) 1725 c.Assert(results, HasLen, 0) 1726 c.Check(err, DeepEquals, &store.SnapActionError{ 1727 Refresh: map[string]error{ 1728 "hello-world": &store.RevisionNotAvailableError{ 1729 Action: "refresh", 1730 Channel: "stable", 1731 }, 1732 "snap2": &store.RevisionNotAvailableError{ 1733 Action: "refresh", 1734 Channel: "candidate", 1735 Releases: []channel.Channel{ 1736 snaptest.MustParseChannel("beta", "amd64"), 1737 snaptest.MustParseChannel("beta", "arm64"), 1738 }, 1739 }, 1740 }, 1741 Install: map[string]error{ 1742 "foo": &store.RevisionNotAvailableError{ 1743 Action: "install", 1744 Channel: "stable", 1745 }, 1746 }, 1747 Download: map[string]error{ 1748 "bar": &store.RevisionNotAvailableError{ 1749 Action: "download", 1750 Channel: "", 1751 }, 1752 }, 1753 }) 1754 } 1755 1756 func (s *storeActionSuite) TestSnapActionSnapNotFound(c *C) { 1757 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1758 assertRequest(c, r, "POST", snapActionPath) 1759 // check device authorization is set, implicitly checking doRequest was used 1760 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1761 1762 jsonReq, err := ioutil.ReadAll(r.Body) 1763 c.Assert(err, IsNil) 1764 var req struct { 1765 Context []map[string]interface{} `json:"context"` 1766 Actions []map[string]interface{} `json:"actions"` 1767 } 1768 1769 err = json.Unmarshal(jsonReq, &req) 1770 c.Assert(err, IsNil) 1771 1772 c.Assert(req.Context, HasLen, 1) 1773 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1774 "snap-id": helloWorldSnapID, 1775 "instance-key": helloWorldSnapID, 1776 "revision": float64(26), 1777 "tracking-channel": "stable", 1778 "refreshed-date": helloRefreshedDateStr, 1779 "epoch": iZeroEpoch, 1780 }) 1781 c.Assert(req.Actions, HasLen, 3) 1782 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1783 "action": "refresh", 1784 "instance-key": helloWorldSnapID, 1785 "snap-id": helloWorldSnapID, 1786 "channel": "stable", 1787 }) 1788 c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ 1789 "action": "install", 1790 "instance-key": "install-1", 1791 "name": "foo", 1792 "channel": "stable", 1793 "epoch": nil, 1794 }) 1795 c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ 1796 "action": "download", 1797 "instance-key": "download-1", 1798 "name": "bar", 1799 "revision": 42., 1800 "epoch": nil, 1801 }) 1802 1803 io.WriteString(w, `{ 1804 "results": [{ 1805 "result": "error", 1806 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1807 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1808 "error": { 1809 "code": "id-not-found", 1810 "message": "msg1" 1811 } 1812 }, { 1813 "result": "error", 1814 "instance-key": "install-1", 1815 "name": "foo", 1816 "error": { 1817 "code": "name-not-found", 1818 "message": "msg2" 1819 } 1820 }, { 1821 "result": "error", 1822 "instance-key": "download-1", 1823 "name": "bar", 1824 "error": { 1825 "code": "name-not-found", 1826 "message": "msg3" 1827 } 1828 }] 1829 }`) 1830 })) 1831 1832 c.Assert(mockServer, NotNil) 1833 defer mockServer.Close() 1834 1835 mockServerURL, _ := url.Parse(mockServer.URL) 1836 cfg := store.Config{ 1837 StoreBaseURL: mockServerURL, 1838 } 1839 dauthCtx := &testDauthContext{c: c, device: s.device} 1840 sto := store.New(&cfg, dauthCtx) 1841 1842 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1843 { 1844 InstanceName: "hello-world", 1845 SnapID: helloWorldSnapID, 1846 TrackingChannel: "stable", 1847 Revision: snap.R(26), 1848 RefreshedDate: helloRefreshedDate, 1849 }, 1850 }, []*store.SnapAction{ 1851 { 1852 Action: "refresh", 1853 SnapID: helloWorldSnapID, 1854 InstanceName: "hello-world", 1855 Channel: "stable", 1856 }, { 1857 Action: "install", 1858 InstanceName: "foo", 1859 Channel: "stable", 1860 }, { 1861 Action: "download", 1862 InstanceName: "bar", 1863 Revision: snap.R(42), 1864 }, 1865 }, nil, nil, nil) 1866 c.Assert(results, HasLen, 0) 1867 c.Check(err, DeepEquals, &store.SnapActionError{ 1868 Refresh: map[string]error{ 1869 "hello-world": store.ErrSnapNotFound, 1870 }, 1871 Install: map[string]error{ 1872 "foo": store.ErrSnapNotFound, 1873 }, 1874 Download: map[string]error{ 1875 "bar": store.ErrSnapNotFound, 1876 }, 1877 }) 1878 } 1879 1880 func (s *storeActionSuite) TestSnapActionOtherErrors(c *C) { 1881 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1882 assertRequest(c, r, "POST", snapActionPath) 1883 // check device authorization is set, implicitly checking doRequest was used 1884 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1885 1886 jsonReq, err := ioutil.ReadAll(r.Body) 1887 c.Assert(err, IsNil) 1888 var req struct { 1889 Context []map[string]interface{} `json:"context"` 1890 Actions []map[string]interface{} `json:"actions"` 1891 } 1892 1893 err = json.Unmarshal(jsonReq, &req) 1894 c.Assert(err, IsNil) 1895 1896 c.Assert(req.Context, HasLen, 0) 1897 c.Assert(req.Actions, HasLen, 1) 1898 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1899 "action": "install", 1900 "instance-key": "install-1", 1901 "name": "foo", 1902 "channel": "stable", 1903 "epoch": nil, 1904 }) 1905 1906 io.WriteString(w, `{ 1907 "results": [{ 1908 "result": "error", 1909 "error": { 1910 "code": "other1", 1911 "message": "other error one" 1912 } 1913 }], 1914 "error-list": [ 1915 {"code": "global-error", "message": "global error"} 1916 ] 1917 }`) 1918 })) 1919 1920 c.Assert(mockServer, NotNil) 1921 defer mockServer.Close() 1922 1923 mockServerURL, _ := url.Parse(mockServer.URL) 1924 cfg := store.Config{ 1925 StoreBaseURL: mockServerURL, 1926 } 1927 dauthCtx := &testDauthContext{c: c, device: s.device} 1928 sto := store.New(&cfg, dauthCtx) 1929 1930 results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{ 1931 { 1932 Action: "install", 1933 InstanceName: "foo", 1934 Channel: "stable", 1935 }, 1936 }, nil, nil, nil) 1937 c.Assert(results, HasLen, 0) 1938 c.Check(err, DeepEquals, &store.SnapActionError{ 1939 Other: []error{ 1940 fmt.Errorf("other error one"), 1941 fmt.Errorf("global error"), 1942 }, 1943 }) 1944 } 1945 1946 func (s *storeActionSuite) TestSnapActionUnknownAction(c *C) { 1947 restore := release.MockOnClassic(false) 1948 defer restore() 1949 1950 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1951 c.Fatal("should not have made it to the server") 1952 })) 1953 1954 c.Assert(mockServer, NotNil) 1955 defer mockServer.Close() 1956 1957 mockServerURL, _ := url.Parse(mockServer.URL) 1958 cfg := store.Config{ 1959 StoreBaseURL: mockServerURL, 1960 } 1961 dauthCtx := &testDauthContext{c: c, device: s.device} 1962 sto := store.New(&cfg, dauthCtx) 1963 1964 results, _, err := sto.SnapAction(s.ctx, nil, 1965 []*store.SnapAction{ 1966 { 1967 Action: "something unexpected", 1968 InstanceName: "hello-world", 1969 }, 1970 }, nil, nil, nil) 1971 c.Assert(err, ErrorMatches, `.* unsupported action .*`) 1972 c.Assert(results, IsNil) 1973 } 1974 1975 func (s *storeActionSuite) TestSnapActionErrorError(c *C) { 1976 e := &store.SnapActionError{Refresh: map[string]error{ 1977 "foo": fmt.Errorf("sad refresh"), 1978 }} 1979 c.Check(e.Error(), Equals, `cannot refresh snap "foo": sad refresh`) 1980 1981 op, name, err := e.SingleOpError() 1982 c.Check(op, Equals, "refresh") 1983 c.Check(name, Equals, "foo") 1984 c.Check(err, ErrorMatches, "sad refresh") 1985 1986 e = &store.SnapActionError{Refresh: map[string]error{ 1987 "foo": fmt.Errorf("sad refresh 1"), 1988 "bar": fmt.Errorf("sad refresh 2"), 1989 }} 1990 errMsg := e.Error() 1991 c.Check(strings.HasPrefix(errMsg, "cannot refresh:"), Equals, true) 1992 c.Check(errMsg, testutil.Contains, "\nsad refresh 1: \"foo\"") 1993 c.Check(errMsg, testutil.Contains, "\nsad refresh 2: \"bar\"") 1994 1995 op, name, err = e.SingleOpError() 1996 c.Check(op, Equals, "") 1997 c.Check(name, Equals, "") 1998 c.Check(err, IsNil) 1999 2000 e = &store.SnapActionError{Install: map[string]error{ 2001 "foo": fmt.Errorf("sad install"), 2002 }} 2003 c.Check(e.Error(), Equals, `cannot install snap "foo": sad install`) 2004 2005 op, name, err = e.SingleOpError() 2006 c.Check(op, Equals, "install") 2007 c.Check(name, Equals, "foo") 2008 c.Check(err, ErrorMatches, "sad install") 2009 2010 e = &store.SnapActionError{Install: map[string]error{ 2011 "foo": fmt.Errorf("sad install 1"), 2012 "bar": fmt.Errorf("sad install 2"), 2013 }} 2014 errMsg = e.Error() 2015 c.Check(strings.HasPrefix(errMsg, "cannot install:\n"), Equals, true) 2016 c.Check(errMsg, testutil.Contains, "\nsad install 1: \"foo\"") 2017 c.Check(errMsg, testutil.Contains, "\nsad install 2: \"bar\"") 2018 2019 op, name, err = e.SingleOpError() 2020 c.Check(op, Equals, "") 2021 c.Check(name, Equals, "") 2022 c.Check(err, IsNil) 2023 2024 e = &store.SnapActionError{Download: map[string]error{ 2025 "foo": fmt.Errorf("sad download"), 2026 }} 2027 c.Check(e.Error(), Equals, `cannot download snap "foo": sad download`) 2028 2029 op, name, err = e.SingleOpError() 2030 c.Check(op, Equals, "download") 2031 c.Check(name, Equals, "foo") 2032 c.Check(err, ErrorMatches, "sad download") 2033 2034 e = &store.SnapActionError{Download: map[string]error{ 2035 "foo": fmt.Errorf("sad download 1"), 2036 "bar": fmt.Errorf("sad download 2"), 2037 }} 2038 errMsg = e.Error() 2039 c.Check(strings.HasPrefix(errMsg, "cannot download:\n"), Equals, true) 2040 c.Check(errMsg, testutil.Contains, "\nsad download 1: \"foo\"") 2041 c.Check(errMsg, testutil.Contains, "\nsad download 2: \"bar\"") 2042 2043 op, name, err = e.SingleOpError() 2044 c.Check(op, Equals, "") 2045 c.Check(name, Equals, "") 2046 c.Check(err, IsNil) 2047 2048 e = &store.SnapActionError{Refresh: map[string]error{ 2049 "foo": fmt.Errorf("sad refresh 1"), 2050 }, 2051 Install: map[string]error{ 2052 "bar": fmt.Errorf("sad install 2"), 2053 }} 2054 c.Check(e.Error(), Equals, `cannot refresh or install: 2055 sad refresh 1: "foo" 2056 sad install 2: "bar"`) 2057 2058 op, name, err = e.SingleOpError() 2059 c.Check(op, Equals, "") 2060 c.Check(name, Equals, "") 2061 c.Check(err, IsNil) 2062 2063 e = &store.SnapActionError{Refresh: map[string]error{ 2064 "foo": fmt.Errorf("sad refresh 1"), 2065 }, 2066 Download: map[string]error{ 2067 "bar": fmt.Errorf("sad download 2"), 2068 }} 2069 c.Check(e.Error(), Equals, `cannot refresh or download: 2070 sad refresh 1: "foo" 2071 sad download 2: "bar"`) 2072 2073 op, name, err = e.SingleOpError() 2074 c.Check(op, Equals, "") 2075 c.Check(name, Equals, "") 2076 c.Check(err, IsNil) 2077 2078 e = &store.SnapActionError{Install: map[string]error{ 2079 "foo": fmt.Errorf("sad install 1"), 2080 }, 2081 Download: map[string]error{ 2082 "bar": fmt.Errorf("sad download 2"), 2083 }} 2084 c.Check(e.Error(), Equals, `cannot install or download: 2085 sad install 1: "foo" 2086 sad download 2: "bar"`) 2087 2088 op, name, err = e.SingleOpError() 2089 c.Check(op, Equals, "") 2090 c.Check(name, Equals, "") 2091 c.Check(err, IsNil) 2092 2093 e = &store.SnapActionError{Refresh: map[string]error{ 2094 "foo": fmt.Errorf("sad refresh 1"), 2095 }, 2096 Install: map[string]error{ 2097 "bar": fmt.Errorf("sad install 2"), 2098 }, 2099 Download: map[string]error{ 2100 "baz": fmt.Errorf("sad download 3"), 2101 }} 2102 c.Check(e.Error(), Equals, `cannot refresh, install, or download: 2103 sad refresh 1: "foo" 2104 sad install 2: "bar" 2105 sad download 3: "baz"`) 2106 2107 op, name, err = e.SingleOpError() 2108 c.Check(op, Equals, "") 2109 c.Check(name, Equals, "") 2110 c.Check(err, IsNil) 2111 2112 e = &store.SnapActionError{ 2113 NoResults: true, 2114 Other: []error{fmt.Errorf("other error")}, 2115 } 2116 c.Check(e.Error(), Equals, `cannot refresh, install, or download: other error`) 2117 2118 op, name, err = e.SingleOpError() 2119 c.Check(op, Equals, "") 2120 c.Check(name, Equals, "") 2121 c.Check(err, IsNil) 2122 2123 e = &store.SnapActionError{ 2124 Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")}, 2125 } 2126 c.Check(e.Error(), Equals, `cannot refresh, install, or download: 2127 other error 1 2128 other error 2`) 2129 2130 op, name, err = e.SingleOpError() 2131 c.Check(op, Equals, "") 2132 c.Check(name, Equals, "") 2133 c.Check(err, IsNil) 2134 2135 e = &store.SnapActionError{ 2136 Install: map[string]error{ 2137 "bar": fmt.Errorf("sad install"), 2138 }, 2139 Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")}, 2140 } 2141 c.Check(e.Error(), Equals, `cannot refresh, install, or download: 2142 sad install: "bar" 2143 other error 1 2144 other error 2`) 2145 2146 op, name, err = e.SingleOpError() 2147 c.Check(op, Equals, "") 2148 c.Check(name, Equals, "") 2149 c.Check(err, IsNil) 2150 2151 e = &store.SnapActionError{ 2152 NoResults: true, 2153 } 2154 c.Check(e.Error(), Equals, "no install/refresh information results from the store") 2155 2156 op, name, err = e.SingleOpError() 2157 c.Check(op, Equals, "") 2158 c.Check(name, Equals, "") 2159 c.Check(err, IsNil) 2160 } 2161 2162 func (s *storeActionSuite) TestSnapActionRefreshesBothAuths(c *C) { 2163 // snap action (install/refresh) has is its own custom way to 2164 // signal macaroon refreshes that allows to do a best effort 2165 // with the available results 2166 2167 refresh, err := makeTestRefreshDischargeResponse() 2168 c.Assert(err, IsNil) 2169 c.Check(s.user.StoreDischarges[0], Not(Equals), refresh) 2170 2171 // mock refresh response 2172 refreshDischargeEndpointHit := false 2173 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2174 io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh)) 2175 refreshDischargeEndpointHit = true 2176 })) 2177 defer mockSSOServer.Close() 2178 store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh" 2179 2180 refreshSessionRequested := false 2181 expiredAuth := `Macaroon root="expired-session-macaroon"` 2182 n := 0 2183 // mock store response 2184 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2185 c.Check(r.UserAgent(), Equals, userAgent) 2186 2187 switch r.URL.Path { 2188 case snapActionPath: 2189 n++ 2190 type errObj struct { 2191 Code string `json:"code"` 2192 Message string `json:"message"` 2193 } 2194 var errors []errObj 2195 2196 authorization := r.Header.Get("Authorization") 2197 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 2198 if s.user.StoreDischarges[0] != refresh { 2199 errors = append(errors, errObj{Code: "user-authorization-needs-refresh"}) 2200 } 2201 2202 devAuthorization := r.Header.Get("Snap-Device-Authorization") 2203 if devAuthorization == "" { 2204 c.Fatalf("device authentication missing") 2205 } else if devAuthorization == expiredAuth { 2206 errors = append(errors, errObj{Code: "device-authorization-needs-refresh"}) 2207 } else { 2208 c.Check(devAuthorization, Equals, `Macaroon root="refreshed-session-macaroon"`) 2209 } 2210 2211 errorsJSON, err := json.Marshal(errors) 2212 c.Assert(err, IsNil) 2213 2214 io.WriteString(w, fmt.Sprintf(`{ 2215 "results": [{ 2216 "result": "refresh", 2217 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2218 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2219 "name": "hello-world", 2220 "snap": { 2221 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2222 "name": "hello-world", 2223 "revision": 26, 2224 "version": "6.1", 2225 "publisher": { 2226 "id": "canonical", 2227 "name": "canonical", 2228 "title": "Canonical" 2229 } 2230 } 2231 }], 2232 "error-list": %s 2233 }`, errorsJSON)) 2234 case authNoncesPath: 2235 io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) 2236 case authSessionPath: 2237 // sanity of request 2238 jsonReq, err := ioutil.ReadAll(r.Body) 2239 c.Assert(err, IsNil) 2240 var req map[string]string 2241 err = json.Unmarshal(jsonReq, &req) 2242 c.Assert(err, IsNil) 2243 c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) 2244 c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) 2245 c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) 2246 2247 authorization := r.Header.Get("X-Device-Authorization") 2248 if authorization == "" { 2249 c.Fatalf("expecting only refresh") 2250 } else { 2251 c.Check(authorization, Equals, expiredAuth) 2252 io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`) 2253 refreshSessionRequested = true 2254 } 2255 default: 2256 c.Fatalf("unexpected path %q", r.URL.Path) 2257 } 2258 })) 2259 c.Assert(mockServer, NotNil) 2260 defer mockServer.Close() 2261 2262 mockServerURL, _ := url.Parse(mockServer.URL) 2263 2264 // make sure device session is expired 2265 s.device.SessionMacaroon = "expired-session-macaroon" 2266 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 2267 sto := store.New(&store.Config{ 2268 StoreBaseURL: mockServerURL, 2269 }, dauthCtx) 2270 2271 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2272 { 2273 InstanceName: "hello-world", 2274 SnapID: helloWorldSnapID, 2275 TrackingChannel: "beta", 2276 Revision: snap.R(1), 2277 RefreshedDate: helloRefreshedDate, 2278 }, 2279 }, []*store.SnapAction{ 2280 { 2281 Action: "refresh", 2282 SnapID: helloWorldSnapID, 2283 InstanceName: "hello-world", 2284 }, 2285 }, nil, s.user, nil) 2286 c.Assert(err, IsNil) 2287 c.Assert(results, HasLen, 1) 2288 c.Assert(results[0].InstanceName(), Equals, "hello-world") 2289 c.Check(refreshDischargeEndpointHit, Equals, true) 2290 c.Check(refreshSessionRequested, Equals, true) 2291 c.Check(n, Equals, 2) 2292 } 2293 2294 func (s *storeActionSuite) TestSnapActionRefreshParallelInstall(c *C) { 2295 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2296 assertRequest(c, r, "POST", snapActionPath) 2297 // check device authorization is set, implicitly checking doRequest was used 2298 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2299 2300 jsonReq, err := ioutil.ReadAll(r.Body) 2301 c.Assert(err, IsNil) 2302 var req struct { 2303 Context []map[string]interface{} `json:"context"` 2304 Actions []map[string]interface{} `json:"actions"` 2305 } 2306 2307 err = json.Unmarshal(jsonReq, &req) 2308 c.Assert(err, IsNil) 2309 2310 c.Assert(req.Context, HasLen, 2) 2311 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2312 "snap-id": helloWorldSnapID, 2313 "instance-key": helloWorldSnapID, 2314 "revision": float64(26), 2315 "tracking-channel": "stable", 2316 "refreshed-date": helloRefreshedDateStr, 2317 "epoch": iZeroEpoch, 2318 }) 2319 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2320 "snap-id": helloWorldSnapID, 2321 "instance-key": helloWorldFooInstanceKeyWithSalt, 2322 "revision": float64(2), 2323 "tracking-channel": "stable", 2324 "refreshed-date": helloRefreshedDateStr, 2325 "epoch": iZeroEpoch, 2326 }) 2327 c.Assert(req.Actions, HasLen, 1) 2328 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2329 "action": "refresh", 2330 "instance-key": helloWorldFooInstanceKeyWithSalt, 2331 "snap-id": helloWorldSnapID, 2332 "channel": "stable", 2333 }) 2334 2335 io.WriteString(w, `{ 2336 "results": [{ 2337 "result": "refresh", 2338 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk", 2339 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2340 "name": "hello-world", 2341 "snap": { 2342 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2343 "name": "hello-world", 2344 "revision": 26, 2345 "version": "6.1", 2346 "publisher": { 2347 "id": "canonical", 2348 "username": "canonical", 2349 "display-name": "Canonical" 2350 } 2351 } 2352 }] 2353 }`) 2354 })) 2355 2356 c.Assert(mockServer, NotNil) 2357 defer mockServer.Close() 2358 2359 mockServerURL, _ := url.Parse(mockServer.URL) 2360 cfg := store.Config{ 2361 StoreBaseURL: mockServerURL, 2362 } 2363 dauthCtx := &testDauthContext{c: c, device: s.device} 2364 sto := store.New(&cfg, dauthCtx) 2365 2366 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2367 { 2368 InstanceName: "hello-world", 2369 SnapID: helloWorldSnapID, 2370 TrackingChannel: "stable", 2371 Revision: snap.R(26), 2372 RefreshedDate: helloRefreshedDate, 2373 }, { 2374 InstanceName: "hello-world_foo", 2375 SnapID: helloWorldSnapID, 2376 TrackingChannel: "stable", 2377 Revision: snap.R(2), 2378 RefreshedDate: helloRefreshedDate, 2379 }, 2380 }, []*store.SnapAction{ 2381 { 2382 Action: "refresh", 2383 SnapID: helloWorldSnapID, 2384 Channel: "stable", 2385 InstanceName: "hello-world_foo", 2386 }, 2387 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 2388 c.Assert(err, IsNil) 2389 c.Assert(results, HasLen, 1) 2390 c.Assert(results[0].SnapName(), Equals, "hello-world") 2391 c.Assert(results[0].InstanceName(), Equals, "hello-world_foo") 2392 c.Assert(results[0].Revision, Equals, snap.R(26)) 2393 } 2394 2395 func (s *storeActionSuite) TestSnapActionRefreshStableInstanceKey(c *C) { 2396 // salt "foo" 2397 helloWorldFooInstanceKeyWithSaltFoo := helloWorldSnapID + ":CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI" 2398 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2399 assertRequest(c, r, "POST", snapActionPath) 2400 // check device authorization is set, implicitly checking doRequest was used 2401 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2402 2403 jsonReq, err := ioutil.ReadAll(r.Body) 2404 c.Assert(err, IsNil) 2405 var req struct { 2406 Context []map[string]interface{} `json:"context"` 2407 Actions []map[string]interface{} `json:"actions"` 2408 } 2409 2410 err = json.Unmarshal(jsonReq, &req) 2411 c.Assert(err, IsNil) 2412 2413 c.Assert(req.Context, HasLen, 2) 2414 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2415 "snap-id": helloWorldSnapID, 2416 "instance-key": helloWorldSnapID, 2417 "revision": float64(26), 2418 "tracking-channel": "stable", 2419 "refreshed-date": helloRefreshedDateStr, 2420 "epoch": iZeroEpoch, 2421 "cohort-key": "what", 2422 }) 2423 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2424 "snap-id": helloWorldSnapID, 2425 "instance-key": helloWorldFooInstanceKeyWithSaltFoo, 2426 "revision": float64(2), 2427 "tracking-channel": "stable", 2428 "refreshed-date": helloRefreshedDateStr, 2429 "epoch": iZeroEpoch, 2430 }) 2431 c.Assert(req.Actions, HasLen, 1) 2432 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2433 "action": "refresh", 2434 "instance-key": helloWorldFooInstanceKeyWithSaltFoo, 2435 "snap-id": helloWorldSnapID, 2436 "channel": "stable", 2437 }) 2438 2439 io.WriteString(w, `{ 2440 "results": [{ 2441 "result": "refresh", 2442 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI", 2443 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2444 "name": "hello-world", 2445 "snap": { 2446 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2447 "name": "hello-world", 2448 "revision": 26, 2449 "version": "6.1", 2450 "publisher": { 2451 "id": "canonical", 2452 "username": "canonical", 2453 "display-name": "Canonical" 2454 } 2455 } 2456 }] 2457 }`) 2458 })) 2459 2460 c.Assert(mockServer, NotNil) 2461 defer mockServer.Close() 2462 2463 mockServerURL, _ := url.Parse(mockServer.URL) 2464 cfg := store.Config{ 2465 StoreBaseURL: mockServerURL, 2466 } 2467 dauthCtx := &testDauthContext{c: c, device: s.device} 2468 sto := store.New(&cfg, dauthCtx) 2469 2470 opts := &store.RefreshOptions{PrivacyKey: "foo"} 2471 currentSnaps := []*store.CurrentSnap{ 2472 { 2473 InstanceName: "hello-world", 2474 SnapID: helloWorldSnapID, 2475 TrackingChannel: "stable", 2476 Revision: snap.R(26), 2477 RefreshedDate: helloRefreshedDate, 2478 CohortKey: "what", 2479 }, { 2480 InstanceName: "hello-world_foo", 2481 SnapID: helloWorldSnapID, 2482 TrackingChannel: "stable", 2483 Revision: snap.R(2), 2484 RefreshedDate: helloRefreshedDate, 2485 }, 2486 } 2487 action := []*store.SnapAction{ 2488 { 2489 Action: "refresh", 2490 SnapID: helloWorldSnapID, 2491 Channel: "stable", 2492 InstanceName: "hello-world_foo", 2493 }, 2494 } 2495 results, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts) 2496 c.Assert(err, IsNil) 2497 c.Assert(results, HasLen, 1) 2498 c.Assert(results[0].SnapName(), Equals, "hello-world") 2499 c.Assert(results[0].InstanceName(), Equals, "hello-world_foo") 2500 c.Assert(results[0].Revision, Equals, snap.R(26)) 2501 2502 // another request with the same seed, gives same result 2503 resultsAgain, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts) 2504 c.Assert(err, IsNil) 2505 c.Assert(resultsAgain, DeepEquals, results) 2506 } 2507 2508 func (s *storeActionSuite) TestSnapActionRefreshWithValidationSets(c *C) { 2509 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2510 assertRequest(c, r, "POST", snapActionPath) 2511 // check device authorization is set, implicitly checking doRequest was used 2512 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2513 2514 jsonReq, err := ioutil.ReadAll(r.Body) 2515 c.Assert(err, IsNil) 2516 var req struct { 2517 Context []map[string]interface{} `json:"context"` 2518 Actions []map[string]interface{} `json:"actions"` 2519 } 2520 2521 err = json.Unmarshal(jsonReq, &req) 2522 c.Assert(err, IsNil) 2523 2524 c.Assert(req.Context, HasLen, 1) 2525 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2526 "snap-id": helloWorldSnapID, 2527 "instance-key": helloWorldSnapID, 2528 "revision": float64(1), 2529 "tracking-channel": "stable", 2530 "refreshed-date": helloRefreshedDateStr, 2531 "epoch": iZeroEpoch, 2532 "validation-sets": []interface{}{[]interface{}{"foo", "bar"}, []interface{}{"foo", "baz"}}, 2533 }) 2534 c.Assert(req.Actions, HasLen, 1) 2535 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2536 "action": "refresh", 2537 "instance-key": helloWorldSnapID, 2538 "snap-id": helloWorldSnapID, 2539 "channel": "stable", 2540 }) 2541 2542 io.WriteString(w, `{ 2543 "results": [{ 2544 "result": "refresh", 2545 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2546 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2547 "name": "hello-world", 2548 "snap": { 2549 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2550 "name": "hello-world", 2551 "revision": 26, 2552 "version": "6.1", 2553 "publisher": { 2554 "id": "canonical", 2555 "username": "canonical", 2556 "display-name": "Canonical" 2557 } 2558 } 2559 }] 2560 }`) 2561 })) 2562 2563 c.Assert(mockServer, NotNil) 2564 defer mockServer.Close() 2565 2566 mockServerURL, _ := url.Parse(mockServer.URL) 2567 cfg := store.Config{ 2568 StoreBaseURL: mockServerURL, 2569 } 2570 dauthCtx := &testDauthContext{c: c, device: s.device} 2571 sto := store.New(&cfg, dauthCtx) 2572 2573 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2574 { 2575 InstanceName: "hello-world", 2576 SnapID: helloWorldSnapID, 2577 TrackingChannel: "stable", 2578 Revision: snap.R(1), 2579 RefreshedDate: helloRefreshedDate, 2580 ValidationSets: [][]string{{"foo", "bar"}, {"foo", "baz"}}, 2581 }, 2582 }, []*store.SnapAction{ 2583 { 2584 Action: "refresh", 2585 SnapID: helloWorldSnapID, 2586 Channel: "stable", 2587 InstanceName: "hello-world", 2588 }, 2589 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 2590 c.Assert(err, IsNil) 2591 c.Assert(results, HasLen, 1) 2592 c.Assert(results[0].SnapName(), Equals, "hello-world") 2593 c.Assert(results[0].InstanceName(), Equals, "hello-world") 2594 c.Assert(results[0].Revision, Equals, snap.R(26)) 2595 } 2596 2597 func (s *storeActionSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) { 2598 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2599 assertRequest(c, r, "POST", snapActionPath) 2600 // check device authorization is set, implicitly checking doRequest was used 2601 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2602 2603 jsonReq, err := ioutil.ReadAll(r.Body) 2604 c.Assert(err, IsNil) 2605 var req struct { 2606 Context []map[string]interface{} `json:"context"` 2607 Actions []map[string]interface{} `json:"actions"` 2608 } 2609 2610 err = json.Unmarshal(jsonReq, &req) 2611 c.Assert(err, IsNil) 2612 2613 c.Assert(req.Context, HasLen, 2) 2614 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2615 "snap-id": helloWorldSnapID, 2616 "instance-key": helloWorldSnapID, 2617 "revision": float64(26), 2618 "tracking-channel": "stable", 2619 "refreshed-date": helloRefreshedDateStr, 2620 "epoch": iZeroEpoch, 2621 }) 2622 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2623 "snap-id": helloWorldSnapID, 2624 "instance-key": helloWorldFooInstanceKeyWithSalt, 2625 "revision": float64(2), 2626 "tracking-channel": "edge", 2627 "refreshed-date": helloRefreshedDateStr, 2628 "epoch": iZeroEpoch, 2629 }) 2630 c.Assert(req.Actions, HasLen, 3) 2631 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2632 "action": "refresh", 2633 "instance-key": helloWorldSnapID, 2634 "snap-id": helloWorldSnapID, 2635 }) 2636 c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ 2637 "action": "refresh", 2638 "instance-key": helloWorldFooInstanceKeyWithSalt, 2639 "snap-id": helloWorldSnapID, 2640 }) 2641 c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ 2642 "action": "install", 2643 "instance-key": "install-1", 2644 "name": "other", 2645 "channel": "stable", 2646 "epoch": nil, 2647 }) 2648 2649 io.WriteString(w, `{ 2650 "results": [{ 2651 "result": "error", 2652 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2653 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2654 "name": "hello-world", 2655 "error": { 2656 "code": "revision-not-found", 2657 "message": "msg1" 2658 } 2659 }, { 2660 "result": "error", 2661 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk", 2662 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2663 "name": "hello-world", 2664 "error": { 2665 "code": "revision-not-found", 2666 "message": "msg2" 2667 } 2668 }, { 2669 "result": "error", 2670 "instance-key": "install-1", 2671 "snap-id": "foo-id", 2672 "name": "other", 2673 "error": { 2674 "code": "revision-not-found", 2675 "message": "msg3" 2676 } 2677 } 2678 ] 2679 }`) 2680 })) 2681 2682 c.Assert(mockServer, NotNil) 2683 defer mockServer.Close() 2684 2685 mockServerURL, _ := url.Parse(mockServer.URL) 2686 cfg := store.Config{ 2687 StoreBaseURL: mockServerURL, 2688 } 2689 dauthCtx := &testDauthContext{c: c, device: s.device} 2690 sto := store.New(&cfg, dauthCtx) 2691 2692 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2693 { 2694 InstanceName: "hello-world", 2695 SnapID: helloWorldSnapID, 2696 TrackingChannel: "stable", 2697 Revision: snap.R(26), 2698 RefreshedDate: helloRefreshedDate, 2699 }, 2700 { 2701 InstanceName: "hello-world_foo", 2702 SnapID: helloWorldSnapID, 2703 TrackingChannel: "edge", 2704 Revision: snap.R(2), 2705 RefreshedDate: helloRefreshedDate, 2706 }, 2707 }, []*store.SnapAction{ 2708 { 2709 Action: "refresh", 2710 InstanceName: "hello-world", 2711 SnapID: helloWorldSnapID, 2712 }, { 2713 Action: "refresh", 2714 InstanceName: "hello-world_foo", 2715 SnapID: helloWorldSnapID, 2716 }, { 2717 Action: "install", 2718 InstanceName: "other_foo", 2719 Channel: "stable", 2720 }, 2721 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 2722 c.Assert(results, HasLen, 0) 2723 c.Check(err, DeepEquals, &store.SnapActionError{ 2724 Refresh: map[string]error{ 2725 "hello-world": &store.RevisionNotAvailableError{ 2726 Action: "refresh", 2727 Channel: "stable", 2728 }, 2729 "hello-world_foo": &store.RevisionNotAvailableError{ 2730 Action: "refresh", 2731 Channel: "edge", 2732 }, 2733 }, 2734 Install: map[string]error{ 2735 "other_foo": &store.RevisionNotAvailableError{ 2736 Action: "install", 2737 Channel: "stable", 2738 }, 2739 }, 2740 }) 2741 } 2742 2743 func (s *storeActionSuite) TestSnapActionInstallParallelInstall(c *C) { 2744 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2745 assertRequest(c, r, "POST", snapActionPath) 2746 // check device authorization is set, implicitly checking doRequest was used 2747 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2748 2749 jsonReq, err := ioutil.ReadAll(r.Body) 2750 c.Assert(err, IsNil) 2751 var req struct { 2752 Context []map[string]interface{} `json:"context"` 2753 Actions []map[string]interface{} `json:"actions"` 2754 } 2755 2756 err = json.Unmarshal(jsonReq, &req) 2757 c.Assert(err, IsNil) 2758 2759 c.Assert(req.Context, HasLen, 1) 2760 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2761 "snap-id": helloWorldSnapID, 2762 "instance-key": helloWorldSnapID, 2763 "revision": float64(26), 2764 "tracking-channel": "stable", 2765 "refreshed-date": helloRefreshedDateStr, 2766 "epoch": iZeroEpoch, 2767 }) 2768 c.Assert(req.Actions, HasLen, 1) 2769 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2770 "action": "install", 2771 "instance-key": "install-1", 2772 "name": "hello-world", 2773 "channel": "stable", 2774 "epoch": nil, 2775 }) 2776 2777 io.WriteString(w, `{ 2778 "results": [{ 2779 "result": "install", 2780 "instance-key": "install-1", 2781 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2782 "name": "hello-world", 2783 "snap": { 2784 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2785 "name": "hello-world", 2786 "revision": 28, 2787 "version": "6.1", 2788 "publisher": { 2789 "id": "canonical", 2790 "username": "canonical", 2791 "display-name": "Canonical" 2792 } 2793 } 2794 }] 2795 }`) 2796 })) 2797 2798 c.Assert(mockServer, NotNil) 2799 defer mockServer.Close() 2800 2801 mockServerURL, _ := url.Parse(mockServer.URL) 2802 cfg := store.Config{ 2803 StoreBaseURL: mockServerURL, 2804 } 2805 dauthCtx := &testDauthContext{c: c, device: s.device} 2806 sto := store.New(&cfg, dauthCtx) 2807 2808 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2809 { 2810 InstanceName: "hello-world", 2811 SnapID: helloWorldSnapID, 2812 TrackingChannel: "stable", 2813 Revision: snap.R(26), 2814 RefreshedDate: helloRefreshedDate, 2815 }, 2816 }, []*store.SnapAction{ 2817 { 2818 Action: "install", 2819 InstanceName: "hello-world_foo", 2820 Channel: "stable", 2821 }, 2822 }, nil, nil, nil) 2823 c.Assert(err, IsNil) 2824 c.Assert(results, HasLen, 1) 2825 c.Assert(results[0].InstanceName(), Equals, "hello-world_foo") 2826 c.Assert(results[0].SnapName(), Equals, "hello-world") 2827 c.Assert(results[0].Revision, Equals, snap.R(28)) 2828 c.Assert(results[0].Version, Equals, "6.1") 2829 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 2830 c.Assert(results[0].Deltas, HasLen, 0) 2831 // effective-channel is not set 2832 c.Assert(results[0].Channel, Equals, "") 2833 } 2834 2835 func (s *storeActionSuite) TestSnapActionErrorsWhenNoInstanceName(c *C) { 2836 dauthCtx := &testDauthContext{c: c, device: s.device} 2837 sto := store.New(&store.Config{}, dauthCtx) 2838 2839 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2840 { 2841 InstanceName: "hello-world", 2842 SnapID: helloWorldSnapID, 2843 TrackingChannel: "stable", 2844 Revision: snap.R(26), 2845 RefreshedDate: helloRefreshedDate, 2846 }, 2847 }, []*store.SnapAction{ 2848 { 2849 Action: "install", 2850 Channel: "stable", 2851 }, 2852 }, nil, nil, nil) 2853 c.Assert(err, ErrorMatches, "internal error: action without instance name") 2854 c.Assert(results, IsNil) 2855 } 2856 2857 func (s *storeActionSuite) TestSnapActionInstallUnexpectedInstallKey(c *C) { 2858 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2859 assertRequest(c, r, "POST", snapActionPath) 2860 // check device authorization is set, implicitly checking doRequest was used 2861 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2862 2863 jsonReq, err := ioutil.ReadAll(r.Body) 2864 c.Assert(err, IsNil) 2865 var req struct { 2866 Context []map[string]interface{} `json:"context"` 2867 Actions []map[string]interface{} `json:"actions"` 2868 } 2869 2870 err = json.Unmarshal(jsonReq, &req) 2871 c.Assert(err, IsNil) 2872 2873 c.Assert(req.Context, HasLen, 1) 2874 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2875 "snap-id": helloWorldSnapID, 2876 "instance-key": helloWorldSnapID, 2877 "revision": float64(26), 2878 "tracking-channel": "stable", 2879 "refreshed-date": helloRefreshedDateStr, 2880 "epoch": iZeroEpoch, 2881 }) 2882 c.Assert(req.Actions, HasLen, 1) 2883 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2884 "action": "install", 2885 "instance-key": "install-1", 2886 "name": "hello-world", 2887 "channel": "stable", 2888 "epoch": nil, 2889 }) 2890 2891 io.WriteString(w, `{ 2892 "results": [{ 2893 "result": "install", 2894 "instance-key": "foo-2", 2895 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2896 "name": "hello-world", 2897 "snap": { 2898 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2899 "name": "hello-world", 2900 "revision": 28, 2901 "version": "6.1", 2902 "publisher": { 2903 "id": "canonical", 2904 "username": "canonical", 2905 "display-name": "Canonical" 2906 } 2907 } 2908 }] 2909 }`) 2910 })) 2911 2912 c.Assert(mockServer, NotNil) 2913 defer mockServer.Close() 2914 2915 mockServerURL, _ := url.Parse(mockServer.URL) 2916 cfg := store.Config{ 2917 StoreBaseURL: mockServerURL, 2918 } 2919 dauthCtx := &testDauthContext{c: c, device: s.device} 2920 sto := store.New(&cfg, dauthCtx) 2921 2922 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2923 { 2924 InstanceName: "hello-world", 2925 SnapID: helloWorldSnapID, 2926 TrackingChannel: "stable", 2927 Revision: snap.R(26), 2928 RefreshedDate: helloRefreshedDate, 2929 }, 2930 }, []*store.SnapAction{ 2931 { 2932 Action: "install", 2933 InstanceName: "hello-world_foo", 2934 Channel: "stable", 2935 }, 2936 }, nil, nil, nil) 2937 c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected instance-key "foo-2"`) 2938 c.Assert(results, IsNil) 2939 } 2940 2941 func (s *storeActionSuite) TestSnapActionRefreshUnexpectedInstanceKey(c *C) { 2942 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2943 assertRequest(c, r, "POST", snapActionPath) 2944 // check device authorization is set, implicitly checking doRequest was used 2945 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2946 2947 jsonReq, err := ioutil.ReadAll(r.Body) 2948 c.Assert(err, IsNil) 2949 var req struct { 2950 Context []map[string]interface{} `json:"context"` 2951 Actions []map[string]interface{} `json:"actions"` 2952 } 2953 2954 err = json.Unmarshal(jsonReq, &req) 2955 c.Assert(err, IsNil) 2956 2957 c.Assert(req.Context, HasLen, 1) 2958 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2959 "snap-id": helloWorldSnapID, 2960 "instance-key": helloWorldSnapID, 2961 "revision": float64(26), 2962 "tracking-channel": "stable", 2963 "refreshed-date": helloRefreshedDateStr, 2964 "epoch": iZeroEpoch, 2965 }) 2966 c.Assert(req.Actions, HasLen, 1) 2967 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2968 "action": "refresh", 2969 "instance-key": helloWorldSnapID, 2970 "snap-id": helloWorldSnapID, 2971 "channel": "stable", 2972 }) 2973 2974 io.WriteString(w, `{ 2975 "results": [{ 2976 "result": "refresh", 2977 "instance-key": "foo-5", 2978 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2979 "name": "hello-world", 2980 "snap": { 2981 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2982 "name": "hello-world", 2983 "revision": 26, 2984 "version": "6.1", 2985 "publisher": { 2986 "id": "canonical", 2987 "username": "canonical", 2988 "display-name": "Canonical" 2989 } 2990 } 2991 }] 2992 }`) 2993 })) 2994 2995 c.Assert(mockServer, NotNil) 2996 defer mockServer.Close() 2997 2998 mockServerURL, _ := url.Parse(mockServer.URL) 2999 cfg := store.Config{ 3000 StoreBaseURL: mockServerURL, 3001 } 3002 dauthCtx := &testDauthContext{c: c, device: s.device} 3003 sto := store.New(&cfg, dauthCtx) 3004 3005 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 3006 { 3007 InstanceName: "hello-world", 3008 SnapID: helloWorldSnapID, 3009 TrackingChannel: "stable", 3010 Revision: snap.R(26), 3011 RefreshedDate: helloRefreshedDate, 3012 }, 3013 }, []*store.SnapAction{ 3014 { 3015 Action: "refresh", 3016 SnapID: helloWorldSnapID, 3017 Channel: "stable", 3018 InstanceName: "hello-world", 3019 }, 3020 }, nil, nil, nil) 3021 c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected refresh`) 3022 c.Assert(results, IsNil) 3023 } 3024 3025 func (s *storeActionSuite) TestSnapActionUnexpectedErrorKey(c *C) { 3026 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3027 assertRequest(c, r, "POST", snapActionPath) 3028 // check device authorization is set, implicitly checking doRequest was used 3029 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3030 3031 jsonReq, err := ioutil.ReadAll(r.Body) 3032 c.Assert(err, IsNil) 3033 var req struct { 3034 Context []map[string]interface{} `json:"context"` 3035 Actions []map[string]interface{} `json:"actions"` 3036 } 3037 3038 err = json.Unmarshal(jsonReq, &req) 3039 c.Assert(err, IsNil) 3040 3041 c.Assert(req.Context, HasLen, 2) 3042 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 3043 "snap-id": helloWorldSnapID, 3044 "instance-key": helloWorldSnapID, 3045 "revision": float64(26), 3046 "tracking-channel": "stable", 3047 "refreshed-date": helloRefreshedDateStr, 3048 "epoch": iZeroEpoch, 3049 }) 3050 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 3051 "snap-id": helloWorldSnapID, 3052 "instance-key": helloWorldFooInstanceKeyWithSalt, 3053 "revision": float64(2), 3054 "tracking-channel": "stable", 3055 "refreshed-date": helloRefreshedDateStr, 3056 "epoch": iZeroEpoch, 3057 }) 3058 c.Assert(req.Actions, HasLen, 1) 3059 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 3060 "action": "install", 3061 "instance-key": "install-1", 3062 "name": "foo-2", 3063 "epoch": nil, 3064 }) 3065 3066 io.WriteString(w, `{ 3067 "results": [{ 3068 "result": "install", 3069 "instance-key": "install-1", 3070 "snap-id": "foo-2-id", 3071 "name": "foo-2", 3072 "snap": { 3073 "snap-id": "foo-2-id", 3074 "name": "foo-2", 3075 "revision": 28, 3076 "version": "6.1", 3077 "publisher": { 3078 "id": "canonical", 3079 "username": "canonical", 3080 "display-name": "Canonical" 3081 } 3082 } 3083 },{ 3084 "error": { 3085 "code": "duplicated-snap", 3086 "message": "The Snap is present more than once in the request." 3087 }, 3088 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk", 3089 "name": null, 3090 "result": "error", 3091 "snap": null, 3092 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ" 3093 }] 3094 }`) 3095 })) 3096 3097 c.Assert(mockServer, NotNil) 3098 defer mockServer.Close() 3099 3100 mockServerURL, _ := url.Parse(mockServer.URL) 3101 cfg := store.Config{ 3102 StoreBaseURL: mockServerURL, 3103 } 3104 dauthCtx := &testDauthContext{c: c, device: s.device} 3105 sto := store.New(&cfg, dauthCtx) 3106 3107 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 3108 { 3109 InstanceName: "hello-world", 3110 SnapID: helloWorldSnapID, 3111 TrackingChannel: "stable", 3112 Revision: snap.R(26), 3113 RefreshedDate: helloRefreshedDate, 3114 }, { 3115 InstanceName: "hello-world_foo", 3116 SnapID: helloWorldSnapID, 3117 TrackingChannel: "stable", 3118 Revision: snap.R(2), 3119 RefreshedDate: helloRefreshedDate, 3120 }, 3121 }, []*store.SnapAction{ 3122 { 3123 Action: "install", 3124 InstanceName: "foo-2", 3125 }, 3126 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 3127 c.Assert(err, DeepEquals, &store.SnapActionError{ 3128 Other: []error{fmt.Errorf(`snap "hello-world_foo": The Snap is present more than once in the request.`)}, 3129 }) 3130 c.Assert(results, HasLen, 1) 3131 c.Assert(results[0].InstanceName(), Equals, "foo-2") 3132 c.Assert(results[0].SnapID, Equals, "foo-2-id") 3133 } 3134 3135 func (s *storeActionSuite) TestSnapAction500(c *C) { 3136 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3137 assertRequest(c, r, "POST", snapActionPath) 3138 // check device authorization is set, implicitly checking doRequest was used 3139 w.WriteHeader(500) 3140 })) 3141 3142 c.Assert(mockServer, NotNil) 3143 defer mockServer.Close() 3144 3145 mockServerURL, _ := url.Parse(mockServer.URL) 3146 cfg := store.Config{ 3147 StoreBaseURL: mockServerURL, 3148 } 3149 dauthCtx := &testDauthContext{c: c, device: s.device} 3150 sto := store.New(&cfg, dauthCtx) 3151 3152 results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{ 3153 { 3154 Action: "install", 3155 InstanceName: "foo", 3156 }, 3157 }, nil, nil, nil) 3158 c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://127\.0\.0\.1:.*/v2/snaps/refresh"`) 3159 c.Check(err, FitsTypeOf, &store.UnexpectedHTTPStatusError{}) 3160 c.Check(results, HasLen, 0) 3161 } 3162 3163 func (s *storeActionSuite) TestSnapAction400(c *C) { 3164 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3165 assertRequest(c, r, "POST", snapActionPath) 3166 // check device authorization is set, implicitly checking doRequest was used 3167 w.WriteHeader(400) 3168 })) 3169 3170 c.Assert(mockServer, NotNil) 3171 defer mockServer.Close() 3172 3173 mockServerURL, _ := url.Parse(mockServer.URL) 3174 cfg := store.Config{ 3175 StoreBaseURL: mockServerURL, 3176 } 3177 dauthCtx := &testDauthContext{c: c, device: s.device} 3178 sto := store.New(&cfg, dauthCtx) 3179 3180 results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{ 3181 { 3182 Action: "install", 3183 InstanceName: "foo", 3184 }, 3185 }, nil, nil, nil) 3186 c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 400 via POST to "http://127\.0\.0\.1:.*/v2/snaps/refresh"`) 3187 c.Check(err, FitsTypeOf, &store.UnexpectedHTTPStatusError{}) 3188 c.Check(results, HasLen, 0) 3189 }