github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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) TestSnapActionAutoRefresh(c *C) { 757 // the bare TestSnapAction does more SnapAction checks; look there 758 // this one mostly just checks the refresh-reason header 759 760 restore := release.MockOnClassic(false) 761 defer restore() 762 763 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 764 assertRequest(c, r, "POST", snapActionPath) 765 c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled") 766 767 io.WriteString(w, `{ 768 "results": [{ 769 "result": "refresh", 770 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 771 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 772 "name": "hello-world", 773 "snap": { 774 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 775 "name": "hello-world", 776 "revision": 26, 777 "version": "6.1", 778 "epoch": {"read": [0], "write": [0]}, 779 "publisher": { 780 "id": "canonical", 781 "username": "canonical", 782 "display-name": "Canonical" 783 } 784 } 785 }] 786 }`) 787 })) 788 789 c.Assert(mockServer, NotNil) 790 defer mockServer.Close() 791 792 mockServerURL, _ := url.Parse(mockServer.URL) 793 cfg := store.Config{ 794 StoreBaseURL: mockServerURL, 795 } 796 dauthCtx := &testDauthContext{c: c, device: s.device} 797 sto := store.New(&cfg, dauthCtx) 798 799 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 800 { 801 InstanceName: "hello-world", 802 SnapID: helloWorldSnapID, 803 TrackingChannel: "beta", 804 Revision: snap.R(1), 805 RefreshedDate: helloRefreshedDate, 806 }, 807 }, []*store.SnapAction{ 808 { 809 Action: "refresh", 810 SnapID: helloWorldSnapID, 811 InstanceName: "hello-world", 812 }, 813 }, nil, nil, &store.RefreshOptions{IsAutoRefresh: true}) 814 c.Assert(err, IsNil) 815 c.Assert(results, HasLen, 1) 816 } 817 818 func (s *storeActionSuite) TestInstallFallbackChannelIsStable(c *C) { 819 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 820 assertRequest(c, r, "POST", snapActionPath) 821 // check device authorization is set, implicitly checking doRequest was used 822 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 823 824 jsonReq, err := ioutil.ReadAll(r.Body) 825 c.Assert(err, IsNil) 826 var req struct { 827 Context []map[string]interface{} `json:"context"` 828 Actions []map[string]interface{} `json:"actions"` 829 } 830 831 err = json.Unmarshal(jsonReq, &req) 832 c.Assert(err, IsNil) 833 834 c.Assert(req.Context, HasLen, 1) 835 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 836 "snap-id": helloWorldSnapID, 837 "instance-key": helloWorldSnapID, 838 "revision": float64(1), 839 "tracking-channel": "stable", 840 "refreshed-date": helloRefreshedDateStr, 841 "epoch": iZeroEpoch, 842 }) 843 c.Assert(req.Actions, HasLen, 1) 844 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 845 "action": "refresh", 846 "instance-key": helloWorldSnapID, 847 "snap-id": helloWorldSnapID, 848 }) 849 850 io.WriteString(w, `{ 851 "results": [{ 852 "result": "refresh", 853 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 854 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 855 "name": "hello-world", 856 "snap": { 857 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 858 "name": "hello-world", 859 "revision": 26, 860 "version": "6.1", 861 "publisher": { 862 "id": "canonical", 863 "username": "canonical", 864 "display-name": "Canonical" 865 } 866 } 867 }] 868 }`) 869 })) 870 871 c.Assert(mockServer, NotNil) 872 defer mockServer.Close() 873 874 mockServerURL, _ := url.Parse(mockServer.URL) 875 cfg := store.Config{ 876 StoreBaseURL: mockServerURL, 877 } 878 dauthCtx := &testDauthContext{c: c, device: s.device} 879 sto := store.New(&cfg, dauthCtx) 880 881 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 882 { 883 InstanceName: "hello-world", 884 SnapID: helloWorldSnapID, 885 RefreshedDate: helloRefreshedDate, 886 Revision: snap.R(1), 887 }, 888 }, []*store.SnapAction{ 889 { 890 Action: "refresh", 891 SnapID: helloWorldSnapID, 892 InstanceName: "hello-world", 893 }, 894 }, nil, nil, nil) 895 c.Assert(err, IsNil) 896 c.Assert(results, HasLen, 1) 897 c.Assert(results[0].InstanceName(), Equals, "hello-world") 898 c.Assert(results[0].Revision, Equals, snap.R(26)) 899 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 900 } 901 902 func (s *storeActionSuite) TestSnapActionNonDefaultsHeaders(c *C) { 903 restore := release.MockOnClassic(true) 904 defer restore() 905 906 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 907 assertRequest(c, r, "POST", snapActionPath) 908 // check device authorization is set, implicitly checking doRequest was used 909 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 910 911 storeID := r.Header.Get("Snap-Device-Store") 912 c.Check(storeID, Equals, "foo") 913 914 c.Check(r.Header.Get("Snap-Device-Series"), Equals, "21") 915 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, "archXYZ") 916 c.Check(r.Header.Get("Snap-Classic"), Equals, "true") 917 918 jsonReq, err := ioutil.ReadAll(r.Body) 919 c.Assert(err, IsNil) 920 var req struct { 921 Context []map[string]interface{} `json:"context"` 922 Actions []map[string]interface{} `json:"actions"` 923 } 924 925 err = json.Unmarshal(jsonReq, &req) 926 c.Assert(err, IsNil) 927 928 c.Assert(req.Context, HasLen, 1) 929 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 930 "snap-id": helloWorldSnapID, 931 "instance-key": helloWorldSnapID, 932 "revision": float64(1), 933 "tracking-channel": "beta", 934 "refreshed-date": helloRefreshedDateStr, 935 "epoch": iZeroEpoch, 936 }) 937 c.Assert(req.Actions, HasLen, 1) 938 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 939 "action": "refresh", 940 "instance-key": helloWorldSnapID, 941 "snap-id": helloWorldSnapID, 942 }) 943 944 io.WriteString(w, `{ 945 "results": [{ 946 "result": "refresh", 947 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 948 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 949 "name": "hello-world", 950 "snap": { 951 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 952 "name": "hello-world", 953 "revision": 26, 954 "version": "6.1", 955 "publisher": { 956 "id": "canonical", 957 "username": "canonical", 958 "display-name": "Canonical" 959 } 960 } 961 }] 962 }`) 963 })) 964 965 c.Assert(mockServer, NotNil) 966 defer mockServer.Close() 967 968 mockServerURL, _ := url.Parse(mockServer.URL) 969 cfg := store.DefaultConfig() 970 cfg.StoreBaseURL = mockServerURL 971 cfg.Series = "21" 972 cfg.Architecture = "archXYZ" 973 cfg.StoreID = "foo" 974 dauthCtx := &testDauthContext{c: c, device: s.device} 975 sto := store.New(cfg, dauthCtx) 976 977 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 978 { 979 InstanceName: "hello-world", 980 SnapID: helloWorldSnapID, 981 TrackingChannel: "beta", 982 RefreshedDate: helloRefreshedDate, 983 Revision: snap.R(1), 984 }, 985 }, []*store.SnapAction{ 986 { 987 Action: "refresh", 988 SnapID: helloWorldSnapID, 989 InstanceName: "hello-world", 990 }, 991 }, nil, nil, nil) 992 c.Assert(err, IsNil) 993 c.Assert(results, HasLen, 1) 994 c.Assert(results[0].InstanceName(), Equals, "hello-world") 995 c.Assert(results[0].Revision, Equals, snap.R(26)) 996 c.Assert(results[0].Version, Equals, "6.1") 997 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 998 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 999 c.Assert(results[0].Deltas, HasLen, 0) 1000 } 1001 1002 func (s *storeActionSuite) TestSnapActionWithDeltas(c *C) { 1003 origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL") 1004 defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas) 1005 c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil) 1006 1007 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1008 assertRequest(c, r, "POST", snapActionPath) 1009 // check device authorization is set, implicitly checking doRequest was used 1010 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1011 1012 c.Check(r.Header.Get("Snap-Accept-Delta-Format"), Equals, "xdelta3") 1013 jsonReq, err := ioutil.ReadAll(r.Body) 1014 c.Assert(err, IsNil) 1015 var req struct { 1016 Context []map[string]interface{} `json:"context"` 1017 Actions []map[string]interface{} `json:"actions"` 1018 } 1019 1020 err = json.Unmarshal(jsonReq, &req) 1021 c.Assert(err, IsNil) 1022 1023 c.Assert(req.Context, HasLen, 1) 1024 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1025 "snap-id": helloWorldSnapID, 1026 "instance-key": helloWorldSnapID, 1027 "revision": float64(1), 1028 "tracking-channel": "beta", 1029 "refreshed-date": helloRefreshedDateStr, 1030 "epoch": iZeroEpoch, 1031 }) 1032 c.Assert(req.Actions, HasLen, 1) 1033 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1034 "action": "refresh", 1035 "instance-key": helloWorldSnapID, 1036 "snap-id": helloWorldSnapID, 1037 }) 1038 1039 io.WriteString(w, `{ 1040 "results": [{ 1041 "result": "refresh", 1042 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1043 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1044 "name": "hello-world", 1045 "snap": { 1046 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1047 "name": "hello-world", 1048 "revision": 26, 1049 "version": "6.1", 1050 "publisher": { 1051 "id": "canonical", 1052 "username": "canonical", 1053 "display-name": "Canonical" 1054 } 1055 } 1056 }] 1057 }`) 1058 })) 1059 1060 c.Assert(mockServer, NotNil) 1061 defer mockServer.Close() 1062 1063 mockServerURL, _ := url.Parse(mockServer.URL) 1064 cfg := store.Config{ 1065 StoreBaseURL: mockServerURL, 1066 } 1067 dauthCtx := &testDauthContext{c: c, device: s.device} 1068 sto := store.New(&cfg, dauthCtx) 1069 1070 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1071 { 1072 InstanceName: "hello-world", 1073 SnapID: helloWorldSnapID, 1074 TrackingChannel: "beta", 1075 Revision: snap.R(1), 1076 RefreshedDate: helloRefreshedDate, 1077 }, 1078 }, []*store.SnapAction{ 1079 { 1080 Action: "refresh", 1081 SnapID: helloWorldSnapID, 1082 InstanceName: "hello-world", 1083 }, 1084 }, nil, nil, nil) 1085 c.Assert(err, IsNil) 1086 c.Assert(results, HasLen, 1) 1087 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1088 c.Assert(results[0].Revision, Equals, snap.R(26)) 1089 } 1090 1091 func (s *storeActionSuite) TestSnapActionOptions(c *C) { 1092 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1093 assertRequest(c, r, "POST", snapActionPath) 1094 // check device authorization is set, implicitly checking doRequest was used 1095 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1096 1097 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "true") 1098 1099 jsonReq, err := ioutil.ReadAll(r.Body) 1100 c.Assert(err, IsNil) 1101 var req struct { 1102 Context []map[string]interface{} `json:"context"` 1103 Actions []map[string]interface{} `json:"actions"` 1104 } 1105 1106 err = json.Unmarshal(jsonReq, &req) 1107 c.Assert(err, IsNil) 1108 1109 c.Assert(req.Context, HasLen, 1) 1110 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1111 "snap-id": helloWorldSnapID, 1112 "instance-key": helloWorldSnapID, 1113 "revision": float64(1), 1114 "tracking-channel": "stable", 1115 "refreshed-date": helloRefreshedDateStr, 1116 "epoch": iZeroEpoch, 1117 }) 1118 c.Assert(req.Actions, HasLen, 1) 1119 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1120 "action": "refresh", 1121 "instance-key": helloWorldSnapID, 1122 "snap-id": helloWorldSnapID, 1123 "channel": "stable", 1124 }) 1125 1126 io.WriteString(w, `{ 1127 "results": [{ 1128 "result": "refresh", 1129 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1130 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1131 "name": "hello-world", 1132 "snap": { 1133 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1134 "name": "hello-world", 1135 "revision": 26, 1136 "version": "6.1", 1137 "publisher": { 1138 "id": "canonical", 1139 "username": "canonical", 1140 "display-name": "Canonical" 1141 } 1142 } 1143 }] 1144 }`) 1145 })) 1146 1147 c.Assert(mockServer, NotNil) 1148 defer mockServer.Close() 1149 1150 mockServerURL, _ := url.Parse(mockServer.URL) 1151 cfg := store.Config{ 1152 StoreBaseURL: mockServerURL, 1153 } 1154 dauthCtx := &testDauthContext{c: c, device: s.device} 1155 sto := store.New(&cfg, dauthCtx) 1156 1157 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1158 { 1159 InstanceName: "hello-world", 1160 SnapID: helloWorldSnapID, 1161 TrackingChannel: "stable", 1162 Revision: snap.R(1), 1163 RefreshedDate: helloRefreshedDate, 1164 }, 1165 }, []*store.SnapAction{ 1166 { 1167 Action: "refresh", 1168 SnapID: helloWorldSnapID, 1169 InstanceName: "hello-world", 1170 Channel: "stable", 1171 }, 1172 }, nil, nil, &store.RefreshOptions{RefreshManaged: true}) 1173 c.Assert(err, IsNil) 1174 c.Assert(results, HasLen, 1) 1175 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1176 c.Assert(results[0].Revision, Equals, snap.R(26)) 1177 } 1178 1179 func (s *storeActionSuite) TestSnapActionInstall(c *C) { 1180 s.testSnapActionGet("install", "", "", c) 1181 } 1182 func (s *storeActionSuite) TestSnapActionInstallWithCohort(c *C) { 1183 s.testSnapActionGet("install", "what", "", c) 1184 } 1185 func (s *storeActionSuite) TestSnapActionDownload(c *C) { 1186 s.testSnapActionGet("download", "", "", c) 1187 } 1188 func (s *storeActionSuite) TestSnapActionDownloadWithCohort(c *C) { 1189 s.testSnapActionGet("download", "here", "", c) 1190 } 1191 func (s *storeActionSuite) TestSnapActionInstallRedirect(c *C) { 1192 s.testSnapActionGet("install", "", "2.0/candidate", c) 1193 } 1194 func (s *storeActionSuite) TestSnapActionDownloadRedirect(c *C) { 1195 s.testSnapActionGet("download", "", "2.0/candidate", c) 1196 } 1197 func (s *storeActionSuite) testSnapActionGet(action, cohort, redirectChannel string, c *C) { 1198 // action here is one of install or download 1199 restore := release.MockOnClassic(false) 1200 defer restore() 1201 1202 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1203 assertRequest(c, r, "POST", snapActionPath) 1204 // check device authorization is set, implicitly checking doRequest was used 1205 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1206 1207 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 1208 1209 // no store ID by default 1210 storeID := r.Header.Get("Snap-Device-Store") 1211 c.Check(storeID, Equals, "") 1212 1213 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 1214 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 1215 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 1216 1217 jsonReq, err := ioutil.ReadAll(r.Body) 1218 c.Assert(err, IsNil) 1219 var req struct { 1220 Context []map[string]interface{} `json:"context"` 1221 Actions []map[string]interface{} `json:"actions"` 1222 } 1223 1224 err = json.Unmarshal(jsonReq, &req) 1225 c.Assert(err, IsNil) 1226 1227 c.Assert(req.Context, HasLen, 0) 1228 c.Assert(req.Actions, HasLen, 1) 1229 expectedAction := map[string]interface{}{ 1230 "action": action, 1231 "instance-key": action + "-1", 1232 "name": "hello-world", 1233 "channel": "beta", 1234 "epoch": nil, 1235 } 1236 if cohort != "" { 1237 expectedAction["cohort-key"] = cohort 1238 } 1239 c.Assert(req.Actions[0], DeepEquals, expectedAction) 1240 1241 fmt.Fprintf(w, `{ 1242 "results": [{ 1243 "result": "%s", 1244 "instance-key": "%[1]s-1", 1245 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1246 "name": "hello-world", 1247 "effective-channel": "candidate", 1248 "redirect-channel": "%s", 1249 "snap": { 1250 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1251 "name": "hello-world", 1252 "revision": 26, 1253 "version": "6.1", 1254 "publisher": { 1255 "id": "canonical", 1256 "username": "canonical", 1257 "display-name": "Canonical" 1258 } 1259 } 1260 }] 1261 }`, action, redirectChannel) 1262 })) 1263 1264 c.Assert(mockServer, NotNil) 1265 defer mockServer.Close() 1266 1267 mockServerURL, _ := url.Parse(mockServer.URL) 1268 cfg := store.Config{ 1269 StoreBaseURL: mockServerURL, 1270 } 1271 dauthCtx := &testDauthContext{c: c, device: s.device} 1272 sto := store.New(&cfg, dauthCtx) 1273 1274 results, _, err := sto.SnapAction(s.ctx, nil, 1275 []*store.SnapAction{ 1276 { 1277 Action: action, 1278 InstanceName: "hello-world", 1279 Channel: "beta", 1280 CohortKey: cohort, 1281 }, 1282 }, nil, nil, nil) 1283 c.Assert(err, IsNil) 1284 c.Assert(results, HasLen, 1) 1285 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1286 c.Assert(results[0].Revision, Equals, snap.R(26)) 1287 c.Assert(results[0].Version, Equals, "6.1") 1288 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1289 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1290 c.Assert(results[0].Deltas, HasLen, 0) 1291 // effective-channel 1292 c.Assert(results[0].Channel, Equals, "candidate") 1293 c.Assert(results[0].RedirectChannel, Equals, redirectChannel) 1294 } 1295 1296 func (s *storeActionSuite) TestSnapActionInstallAmend(c *C) { 1297 // this is what amend would look like 1298 restore := release.MockOnClassic(false) 1299 defer restore() 1300 1301 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1302 assertRequest(c, r, "POST", snapActionPath) 1303 // check device authorization is set, implicitly checking doRequest was used 1304 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1305 1306 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 1307 1308 // no store ID by default 1309 storeID := r.Header.Get("Snap-Device-Store") 1310 c.Check(storeID, Equals, "") 1311 1312 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 1313 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 1314 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 1315 1316 jsonReq, err := ioutil.ReadAll(r.Body) 1317 c.Assert(err, IsNil) 1318 var req struct { 1319 Context []map[string]interface{} `json:"context"` 1320 Actions []map[string]interface{} `json:"actions"` 1321 } 1322 1323 err = json.Unmarshal(jsonReq, &req) 1324 c.Assert(err, IsNil) 1325 1326 c.Assert(req.Context, HasLen, 0) 1327 c.Assert(req.Actions, HasLen, 1) 1328 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1329 "action": "install", 1330 "instance-key": "install-1", 1331 "name": "hello-world", 1332 "channel": "beta", 1333 "epoch": map[string]interface{}{"read": []interface{}{0., 1.}, "write": []interface{}{1.}}, 1334 }) 1335 1336 fmt.Fprint(w, `{ 1337 "results": [{ 1338 "result": "install", 1339 "instance-key": "install-1", 1340 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1341 "name": "hello-world", 1342 "effective-channel": "candidate", 1343 "snap": { 1344 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1345 "name": "hello-world", 1346 "revision": 26, 1347 "version": "6.1", 1348 "publisher": { 1349 "id": "canonical", 1350 "username": "canonical", 1351 "display-name": "Canonical" 1352 } 1353 } 1354 }] 1355 }`) 1356 })) 1357 1358 c.Assert(mockServer, NotNil) 1359 defer mockServer.Close() 1360 1361 mockServerURL, _ := url.Parse(mockServer.URL) 1362 cfg := store.Config{ 1363 StoreBaseURL: mockServerURL, 1364 } 1365 dauthCtx := &testDauthContext{c: c, device: s.device} 1366 sto := store.New(&cfg, dauthCtx) 1367 1368 results, _, err := sto.SnapAction(s.ctx, nil, 1369 []*store.SnapAction{ 1370 { 1371 Action: "install", 1372 InstanceName: "hello-world", 1373 Channel: "beta", 1374 Epoch: snap.E("1*"), 1375 }, 1376 }, nil, nil, nil) 1377 c.Assert(err, IsNil) 1378 c.Assert(results, HasLen, 1) 1379 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1380 c.Assert(results[0].Revision, Equals, snap.R(26)) 1381 c.Assert(results[0].Version, Equals, "6.1") 1382 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1383 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1384 c.Assert(results[0].Deltas, HasLen, 0) 1385 // effective-channel 1386 c.Assert(results[0].Channel, Equals, "candidate") 1387 } 1388 1389 func (s *storeActionSuite) TestSnapActionWithClientUserAgent(c *C) { 1390 restore := release.MockOnClassic(false) 1391 defer restore() 1392 1393 serverCalls := 0 1394 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1395 serverCalls++ 1396 assertRequest(c, r, "POST", snapActionPath) 1397 1398 c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, "some-snap-agent/1.0") 1399 1400 io.WriteString(w, `{ 1401 "results": [] 1402 }`) 1403 })) 1404 1405 c.Assert(mockServer, NotNil) 1406 defer mockServer.Close() 1407 1408 mockServerURL, _ := url.Parse(mockServer.URL) 1409 cfg := store.Config{ 1410 StoreBaseURL: mockServerURL, 1411 } 1412 dauthCtx := &testDauthContext{c: c, device: s.device} 1413 sto := store.New(&cfg, dauthCtx) 1414 1415 // to construct the client-user-agent context we need to 1416 // create a req that simulates what the req that the daemon got 1417 r, err := http.NewRequest("POST", "/snapd/api", nil) 1418 r.Header.Set("User-Agent", "some-snap-agent/1.0") 1419 c.Assert(err, IsNil) 1420 ctx := store.WithClientUserAgent(s.ctx, r) 1421 1422 results, _, err := sto.SnapAction(ctx, nil, []*store.SnapAction{{Action: "install", InstanceName: "some-snap"}}, nil, nil, nil) 1423 c.Check(serverCalls, Equals, 1) 1424 c.Check(results, HasLen, 0) 1425 c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true}) 1426 } 1427 1428 func (s *storeActionSuite) TestSnapActionDownloadParallelInstanceKey(c *C) { 1429 // action here is one of install or download 1430 restore := release.MockOnClassic(false) 1431 defer restore() 1432 1433 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1434 c.Fatal("should not be reached") 1435 })) 1436 1437 c.Assert(mockServer, NotNil) 1438 defer mockServer.Close() 1439 1440 mockServerURL, _ := url.Parse(mockServer.URL) 1441 cfg := store.Config{ 1442 StoreBaseURL: mockServerURL, 1443 } 1444 dauthCtx := &testDauthContext{c: c, device: s.device} 1445 sto := store.New(&cfg, dauthCtx) 1446 1447 _, _, err := sto.SnapAction(s.ctx, nil, 1448 []*store.SnapAction{ 1449 { 1450 Action: "download", 1451 InstanceName: "hello-world_foo", 1452 Channel: "beta", 1453 }, 1454 }, nil, nil, nil) 1455 c.Assert(err, ErrorMatches, `internal error: unsupported download with instance name "hello-world_foo"`) 1456 } 1457 1458 func (s *storeActionSuite) TestSnapActionInstallWithRevision(c *C) { 1459 s.testSnapActionGetWithRevision("install", c) 1460 } 1461 1462 func (s *storeActionSuite) TestSnapActionDownloadWithRevision(c *C) { 1463 s.testSnapActionGetWithRevision("download", c) 1464 } 1465 1466 func (s *storeActionSuite) testSnapActionGetWithRevision(action string, c *C) { 1467 // action here is one of install or download 1468 restore := release.MockOnClassic(false) 1469 defer restore() 1470 1471 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1472 assertRequest(c, r, "POST", snapActionPath) 1473 // check device authorization is set, implicitly checking doRequest was used 1474 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1475 1476 c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "") 1477 1478 // no store ID by default 1479 storeID := r.Header.Get("Snap-Device-Store") 1480 c.Check(storeID, Equals, "") 1481 1482 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 1483 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 1484 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 1485 1486 jsonReq, err := ioutil.ReadAll(r.Body) 1487 c.Assert(err, IsNil) 1488 var req struct { 1489 Context []map[string]interface{} `json:"context"` 1490 Actions []map[string]interface{} `json:"actions"` 1491 } 1492 1493 err = json.Unmarshal(jsonReq, &req) 1494 c.Assert(err, IsNil) 1495 1496 c.Assert(req.Context, HasLen, 0) 1497 c.Assert(req.Actions, HasLen, 1) 1498 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1499 "action": action, 1500 "instance-key": action + "-1", 1501 "name": "hello-world", 1502 "revision": float64(28), 1503 "epoch": nil, 1504 }) 1505 1506 fmt.Fprintf(w, `{ 1507 "results": [{ 1508 "result": "%s", 1509 "instance-key": "%[1]s-1", 1510 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1511 "name": "hello-world", 1512 "snap": { 1513 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1514 "name": "hello-world", 1515 "revision": 28, 1516 "version": "6.1", 1517 "publisher": { 1518 "id": "canonical", 1519 "username": "canonical", 1520 "display-name": "Canonical" 1521 } 1522 } 1523 }] 1524 }`, action) 1525 })) 1526 1527 c.Assert(mockServer, NotNil) 1528 defer mockServer.Close() 1529 1530 mockServerURL, _ := url.Parse(mockServer.URL) 1531 cfg := store.Config{ 1532 StoreBaseURL: mockServerURL, 1533 } 1534 dauthCtx := &testDauthContext{c: c, device: s.device} 1535 sto := store.New(&cfg, dauthCtx) 1536 1537 results, _, err := sto.SnapAction(s.ctx, nil, 1538 []*store.SnapAction{ 1539 { 1540 Action: action, 1541 InstanceName: "hello-world", 1542 Revision: snap.R(28), 1543 }, 1544 }, nil, nil, nil) 1545 c.Assert(err, IsNil) 1546 c.Assert(results, HasLen, 1) 1547 c.Assert(results[0].InstanceName(), Equals, "hello-world") 1548 c.Assert(results[0].Revision, Equals, snap.R(28)) 1549 c.Assert(results[0].Version, Equals, "6.1") 1550 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 1551 c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID) 1552 c.Assert(results[0].Deltas, HasLen, 0) 1553 // effective-channel is not set 1554 c.Assert(results[0].Channel, Equals, "") 1555 } 1556 1557 func (s *storeActionSuite) TestSnapActionRevisionNotAvailable(c *C) { 1558 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1559 assertRequest(c, r, "POST", snapActionPath) 1560 // check device authorization is set, implicitly checking doRequest was used 1561 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1562 1563 jsonReq, err := ioutil.ReadAll(r.Body) 1564 c.Assert(err, IsNil) 1565 var req struct { 1566 Context []map[string]interface{} `json:"context"` 1567 Actions []map[string]interface{} `json:"actions"` 1568 } 1569 1570 err = json.Unmarshal(jsonReq, &req) 1571 c.Assert(err, IsNil) 1572 1573 c.Assert(req.Context, HasLen, 2) 1574 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1575 "snap-id": helloWorldSnapID, 1576 "instance-key": helloWorldSnapID, 1577 "revision": float64(26), 1578 "tracking-channel": "stable", 1579 "refreshed-date": helloRefreshedDateStr, 1580 "epoch": iZeroEpoch, 1581 }) 1582 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 1583 "snap-id": "snap2-id", 1584 "instance-key": "snap2-id", 1585 "revision": float64(2), 1586 "tracking-channel": "edge", 1587 "refreshed-date": helloRefreshedDateStr, 1588 "epoch": iZeroEpoch, 1589 }) 1590 c.Assert(req.Actions, HasLen, 4) 1591 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1592 "action": "refresh", 1593 "instance-key": helloWorldSnapID, 1594 "snap-id": helloWorldSnapID, 1595 }) 1596 c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ 1597 "action": "refresh", 1598 "instance-key": "snap2-id", 1599 "snap-id": "snap2-id", 1600 "channel": "candidate", 1601 }) 1602 c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ 1603 "action": "install", 1604 "instance-key": "install-1", 1605 "name": "foo", 1606 "channel": "stable", 1607 "epoch": nil, 1608 }) 1609 c.Assert(req.Actions[3], DeepEquals, map[string]interface{}{ 1610 "action": "download", 1611 "instance-key": "download-1", 1612 "name": "bar", 1613 "revision": 42., 1614 "epoch": nil, 1615 }) 1616 1617 io.WriteString(w, `{ 1618 "results": [{ 1619 "result": "error", 1620 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1621 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1622 "name": "hello-world", 1623 "error": { 1624 "code": "revision-not-found", 1625 "message": "msg1" 1626 } 1627 }, { 1628 "result": "error", 1629 "instance-key": "snap2-id", 1630 "snap-id": "snap2-id", 1631 "name": "snap2", 1632 "error": { 1633 "code": "revision-not-found", 1634 "message": "msg1", 1635 "extra": { 1636 "releases": [{"architecture": "amd64", "channel": "beta"}, 1637 {"architecture": "arm64", "channel": "beta"}] 1638 } 1639 } 1640 }, { 1641 "result": "error", 1642 "instance-key": "install-1", 1643 "snap-id": "foo-id", 1644 "name": "foo", 1645 "error": { 1646 "code": "revision-not-found", 1647 "message": "msg2" 1648 } 1649 }, { 1650 "result": "error", 1651 "instance-key": "download-1", 1652 "snap-id": "bar-id", 1653 "name": "bar", 1654 "error": { 1655 "code": "revision-not-found", 1656 "message": "msg3" 1657 } 1658 }] 1659 }`) 1660 })) 1661 1662 c.Assert(mockServer, NotNil) 1663 defer mockServer.Close() 1664 1665 mockServerURL, _ := url.Parse(mockServer.URL) 1666 cfg := store.Config{ 1667 StoreBaseURL: mockServerURL, 1668 } 1669 dauthCtx := &testDauthContext{c: c, device: s.device} 1670 sto := store.New(&cfg, dauthCtx) 1671 1672 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1673 { 1674 InstanceName: "hello-world", 1675 SnapID: helloWorldSnapID, 1676 TrackingChannel: "stable", 1677 Revision: snap.R(26), 1678 RefreshedDate: helloRefreshedDate, 1679 }, 1680 { 1681 InstanceName: "snap2", 1682 SnapID: "snap2-id", 1683 TrackingChannel: "edge", 1684 Revision: snap.R(2), 1685 RefreshedDate: helloRefreshedDate, 1686 }, 1687 }, []*store.SnapAction{ 1688 { 1689 Action: "refresh", 1690 InstanceName: "hello-world", 1691 SnapID: helloWorldSnapID, 1692 }, { 1693 Action: "refresh", 1694 InstanceName: "snap2", 1695 SnapID: "snap2-id", 1696 Channel: "candidate", 1697 }, { 1698 Action: "install", 1699 InstanceName: "foo", 1700 Channel: "stable", 1701 }, { 1702 Action: "download", 1703 InstanceName: "bar", 1704 Revision: snap.R(42), 1705 }, 1706 }, nil, nil, nil) 1707 c.Assert(results, HasLen, 0) 1708 c.Check(err, DeepEquals, &store.SnapActionError{ 1709 Refresh: map[string]error{ 1710 "hello-world": &store.RevisionNotAvailableError{ 1711 Action: "refresh", 1712 Channel: "stable", 1713 }, 1714 "snap2": &store.RevisionNotAvailableError{ 1715 Action: "refresh", 1716 Channel: "candidate", 1717 Releases: []channel.Channel{ 1718 snaptest.MustParseChannel("beta", "amd64"), 1719 snaptest.MustParseChannel("beta", "arm64"), 1720 }, 1721 }, 1722 }, 1723 Install: map[string]error{ 1724 "foo": &store.RevisionNotAvailableError{ 1725 Action: "install", 1726 Channel: "stable", 1727 }, 1728 }, 1729 Download: map[string]error{ 1730 "bar": &store.RevisionNotAvailableError{ 1731 Action: "download", 1732 Channel: "", 1733 }, 1734 }, 1735 }) 1736 } 1737 1738 func (s *storeActionSuite) TestSnapActionSnapNotFound(c *C) { 1739 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1740 assertRequest(c, r, "POST", snapActionPath) 1741 // check device authorization is set, implicitly checking doRequest was used 1742 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1743 1744 jsonReq, err := ioutil.ReadAll(r.Body) 1745 c.Assert(err, IsNil) 1746 var req struct { 1747 Context []map[string]interface{} `json:"context"` 1748 Actions []map[string]interface{} `json:"actions"` 1749 } 1750 1751 err = json.Unmarshal(jsonReq, &req) 1752 c.Assert(err, IsNil) 1753 1754 c.Assert(req.Context, HasLen, 1) 1755 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 1756 "snap-id": helloWorldSnapID, 1757 "instance-key": helloWorldSnapID, 1758 "revision": float64(26), 1759 "tracking-channel": "stable", 1760 "refreshed-date": helloRefreshedDateStr, 1761 "epoch": iZeroEpoch, 1762 }) 1763 c.Assert(req.Actions, HasLen, 3) 1764 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1765 "action": "refresh", 1766 "instance-key": helloWorldSnapID, 1767 "snap-id": helloWorldSnapID, 1768 "channel": "stable", 1769 }) 1770 c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ 1771 "action": "install", 1772 "instance-key": "install-1", 1773 "name": "foo", 1774 "channel": "stable", 1775 "epoch": nil, 1776 }) 1777 c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ 1778 "action": "download", 1779 "instance-key": "download-1", 1780 "name": "bar", 1781 "revision": 42., 1782 "epoch": nil, 1783 }) 1784 1785 io.WriteString(w, `{ 1786 "results": [{ 1787 "result": "error", 1788 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1789 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1790 "error": { 1791 "code": "id-not-found", 1792 "message": "msg1" 1793 } 1794 }, { 1795 "result": "error", 1796 "instance-key": "install-1", 1797 "name": "foo", 1798 "error": { 1799 "code": "name-not-found", 1800 "message": "msg2" 1801 } 1802 }, { 1803 "result": "error", 1804 "instance-key": "download-1", 1805 "name": "bar", 1806 "error": { 1807 "code": "name-not-found", 1808 "message": "msg3" 1809 } 1810 }] 1811 }`) 1812 })) 1813 1814 c.Assert(mockServer, NotNil) 1815 defer mockServer.Close() 1816 1817 mockServerURL, _ := url.Parse(mockServer.URL) 1818 cfg := store.Config{ 1819 StoreBaseURL: mockServerURL, 1820 } 1821 dauthCtx := &testDauthContext{c: c, device: s.device} 1822 sto := store.New(&cfg, dauthCtx) 1823 1824 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 1825 { 1826 InstanceName: "hello-world", 1827 SnapID: helloWorldSnapID, 1828 TrackingChannel: "stable", 1829 Revision: snap.R(26), 1830 RefreshedDate: helloRefreshedDate, 1831 }, 1832 }, []*store.SnapAction{ 1833 { 1834 Action: "refresh", 1835 SnapID: helloWorldSnapID, 1836 InstanceName: "hello-world", 1837 Channel: "stable", 1838 }, { 1839 Action: "install", 1840 InstanceName: "foo", 1841 Channel: "stable", 1842 }, { 1843 Action: "download", 1844 InstanceName: "bar", 1845 Revision: snap.R(42), 1846 }, 1847 }, nil, nil, nil) 1848 c.Assert(results, HasLen, 0) 1849 c.Check(err, DeepEquals, &store.SnapActionError{ 1850 Refresh: map[string]error{ 1851 "hello-world": store.ErrSnapNotFound, 1852 }, 1853 Install: map[string]error{ 1854 "foo": store.ErrSnapNotFound, 1855 }, 1856 Download: map[string]error{ 1857 "bar": store.ErrSnapNotFound, 1858 }, 1859 }) 1860 } 1861 1862 func (s *storeActionSuite) TestSnapActionOtherErrors(c *C) { 1863 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1864 assertRequest(c, r, "POST", snapActionPath) 1865 // check device authorization is set, implicitly checking doRequest was used 1866 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1867 1868 jsonReq, err := ioutil.ReadAll(r.Body) 1869 c.Assert(err, IsNil) 1870 var req struct { 1871 Context []map[string]interface{} `json:"context"` 1872 Actions []map[string]interface{} `json:"actions"` 1873 } 1874 1875 err = json.Unmarshal(jsonReq, &req) 1876 c.Assert(err, IsNil) 1877 1878 c.Assert(req.Context, HasLen, 0) 1879 c.Assert(req.Actions, HasLen, 1) 1880 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 1881 "action": "install", 1882 "instance-key": "install-1", 1883 "name": "foo", 1884 "channel": "stable", 1885 "epoch": nil, 1886 }) 1887 1888 io.WriteString(w, `{ 1889 "results": [{ 1890 "result": "error", 1891 "error": { 1892 "code": "other1", 1893 "message": "other error one" 1894 } 1895 }], 1896 "error-list": [ 1897 {"code": "global-error", "message": "global error"} 1898 ] 1899 }`) 1900 })) 1901 1902 c.Assert(mockServer, NotNil) 1903 defer mockServer.Close() 1904 1905 mockServerURL, _ := url.Parse(mockServer.URL) 1906 cfg := store.Config{ 1907 StoreBaseURL: mockServerURL, 1908 } 1909 dauthCtx := &testDauthContext{c: c, device: s.device} 1910 sto := store.New(&cfg, dauthCtx) 1911 1912 results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{ 1913 { 1914 Action: "install", 1915 InstanceName: "foo", 1916 Channel: "stable", 1917 }, 1918 }, nil, nil, nil) 1919 c.Assert(results, HasLen, 0) 1920 c.Check(err, DeepEquals, &store.SnapActionError{ 1921 Other: []error{ 1922 fmt.Errorf("other error one"), 1923 fmt.Errorf("global error"), 1924 }, 1925 }) 1926 } 1927 1928 func (s *storeActionSuite) TestSnapActionUnknownAction(c *C) { 1929 restore := release.MockOnClassic(false) 1930 defer restore() 1931 1932 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1933 c.Fatal("should not have made it to the server") 1934 })) 1935 1936 c.Assert(mockServer, NotNil) 1937 defer mockServer.Close() 1938 1939 mockServerURL, _ := url.Parse(mockServer.URL) 1940 cfg := store.Config{ 1941 StoreBaseURL: mockServerURL, 1942 } 1943 dauthCtx := &testDauthContext{c: c, device: s.device} 1944 sto := store.New(&cfg, dauthCtx) 1945 1946 results, _, err := sto.SnapAction(s.ctx, nil, 1947 []*store.SnapAction{ 1948 { 1949 Action: "something unexpected", 1950 InstanceName: "hello-world", 1951 }, 1952 }, nil, nil, nil) 1953 c.Assert(err, ErrorMatches, `.* unsupported action .*`) 1954 c.Assert(results, IsNil) 1955 } 1956 1957 func (s *storeActionSuite) TestSnapActionErrorError(c *C) { 1958 e := &store.SnapActionError{Refresh: map[string]error{ 1959 "foo": fmt.Errorf("sad refresh"), 1960 }} 1961 c.Check(e.Error(), Equals, `cannot refresh snap "foo": sad refresh`) 1962 1963 op, name, err := e.SingleOpError() 1964 c.Check(op, Equals, "refresh") 1965 c.Check(name, Equals, "foo") 1966 c.Check(err, ErrorMatches, "sad refresh") 1967 1968 e = &store.SnapActionError{Refresh: map[string]error{ 1969 "foo": fmt.Errorf("sad refresh 1"), 1970 "bar": fmt.Errorf("sad refresh 2"), 1971 }} 1972 errMsg := e.Error() 1973 c.Check(strings.HasPrefix(errMsg, "cannot refresh:"), Equals, true) 1974 c.Check(errMsg, testutil.Contains, "\nsad refresh 1: \"foo\"") 1975 c.Check(errMsg, testutil.Contains, "\nsad refresh 2: \"bar\"") 1976 1977 op, name, err = e.SingleOpError() 1978 c.Check(op, Equals, "") 1979 c.Check(name, Equals, "") 1980 c.Check(err, IsNil) 1981 1982 e = &store.SnapActionError{Install: map[string]error{ 1983 "foo": fmt.Errorf("sad install"), 1984 }} 1985 c.Check(e.Error(), Equals, `cannot install snap "foo": sad install`) 1986 1987 op, name, err = e.SingleOpError() 1988 c.Check(op, Equals, "install") 1989 c.Check(name, Equals, "foo") 1990 c.Check(err, ErrorMatches, "sad install") 1991 1992 e = &store.SnapActionError{Install: map[string]error{ 1993 "foo": fmt.Errorf("sad install 1"), 1994 "bar": fmt.Errorf("sad install 2"), 1995 }} 1996 errMsg = e.Error() 1997 c.Check(strings.HasPrefix(errMsg, "cannot install:\n"), Equals, true) 1998 c.Check(errMsg, testutil.Contains, "\nsad install 1: \"foo\"") 1999 c.Check(errMsg, testutil.Contains, "\nsad install 2: \"bar\"") 2000 2001 op, name, err = e.SingleOpError() 2002 c.Check(op, Equals, "") 2003 c.Check(name, Equals, "") 2004 c.Check(err, IsNil) 2005 2006 e = &store.SnapActionError{Download: map[string]error{ 2007 "foo": fmt.Errorf("sad download"), 2008 }} 2009 c.Check(e.Error(), Equals, `cannot download snap "foo": sad download`) 2010 2011 op, name, err = e.SingleOpError() 2012 c.Check(op, Equals, "download") 2013 c.Check(name, Equals, "foo") 2014 c.Check(err, ErrorMatches, "sad download") 2015 2016 e = &store.SnapActionError{Download: map[string]error{ 2017 "foo": fmt.Errorf("sad download 1"), 2018 "bar": fmt.Errorf("sad download 2"), 2019 }} 2020 errMsg = e.Error() 2021 c.Check(strings.HasPrefix(errMsg, "cannot download:\n"), Equals, true) 2022 c.Check(errMsg, testutil.Contains, "\nsad download 1: \"foo\"") 2023 c.Check(errMsg, testutil.Contains, "\nsad download 2: \"bar\"") 2024 2025 op, name, err = e.SingleOpError() 2026 c.Check(op, Equals, "") 2027 c.Check(name, Equals, "") 2028 c.Check(err, IsNil) 2029 2030 e = &store.SnapActionError{Refresh: map[string]error{ 2031 "foo": fmt.Errorf("sad refresh 1"), 2032 }, 2033 Install: map[string]error{ 2034 "bar": fmt.Errorf("sad install 2"), 2035 }} 2036 c.Check(e.Error(), Equals, `cannot refresh or install: 2037 sad refresh 1: "foo" 2038 sad install 2: "bar"`) 2039 2040 op, name, err = e.SingleOpError() 2041 c.Check(op, Equals, "") 2042 c.Check(name, Equals, "") 2043 c.Check(err, IsNil) 2044 2045 e = &store.SnapActionError{Refresh: map[string]error{ 2046 "foo": fmt.Errorf("sad refresh 1"), 2047 }, 2048 Download: map[string]error{ 2049 "bar": fmt.Errorf("sad download 2"), 2050 }} 2051 c.Check(e.Error(), Equals, `cannot refresh or download: 2052 sad refresh 1: "foo" 2053 sad download 2: "bar"`) 2054 2055 op, name, err = e.SingleOpError() 2056 c.Check(op, Equals, "") 2057 c.Check(name, Equals, "") 2058 c.Check(err, IsNil) 2059 2060 e = &store.SnapActionError{Install: map[string]error{ 2061 "foo": fmt.Errorf("sad install 1"), 2062 }, 2063 Download: map[string]error{ 2064 "bar": fmt.Errorf("sad download 2"), 2065 }} 2066 c.Check(e.Error(), Equals, `cannot install or download: 2067 sad install 1: "foo" 2068 sad download 2: "bar"`) 2069 2070 op, name, err = e.SingleOpError() 2071 c.Check(op, Equals, "") 2072 c.Check(name, Equals, "") 2073 c.Check(err, IsNil) 2074 2075 e = &store.SnapActionError{Refresh: map[string]error{ 2076 "foo": fmt.Errorf("sad refresh 1"), 2077 }, 2078 Install: map[string]error{ 2079 "bar": fmt.Errorf("sad install 2"), 2080 }, 2081 Download: map[string]error{ 2082 "baz": fmt.Errorf("sad download 3"), 2083 }} 2084 c.Check(e.Error(), Equals, `cannot refresh, install, or download: 2085 sad refresh 1: "foo" 2086 sad install 2: "bar" 2087 sad download 3: "baz"`) 2088 2089 op, name, err = e.SingleOpError() 2090 c.Check(op, Equals, "") 2091 c.Check(name, Equals, "") 2092 c.Check(err, IsNil) 2093 2094 e = &store.SnapActionError{ 2095 NoResults: true, 2096 Other: []error{fmt.Errorf("other error")}, 2097 } 2098 c.Check(e.Error(), Equals, `cannot refresh, install, or download: other error`) 2099 2100 op, name, err = e.SingleOpError() 2101 c.Check(op, Equals, "") 2102 c.Check(name, Equals, "") 2103 c.Check(err, IsNil) 2104 2105 e = &store.SnapActionError{ 2106 Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")}, 2107 } 2108 c.Check(e.Error(), Equals, `cannot refresh, install, or download: 2109 other error 1 2110 other error 2`) 2111 2112 op, name, err = e.SingleOpError() 2113 c.Check(op, Equals, "") 2114 c.Check(name, Equals, "") 2115 c.Check(err, IsNil) 2116 2117 e = &store.SnapActionError{ 2118 Install: map[string]error{ 2119 "bar": fmt.Errorf("sad install"), 2120 }, 2121 Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")}, 2122 } 2123 c.Check(e.Error(), Equals, `cannot refresh, install, or download: 2124 sad install: "bar" 2125 other error 1 2126 other error 2`) 2127 2128 op, name, err = e.SingleOpError() 2129 c.Check(op, Equals, "") 2130 c.Check(name, Equals, "") 2131 c.Check(err, IsNil) 2132 2133 e = &store.SnapActionError{ 2134 NoResults: true, 2135 } 2136 c.Check(e.Error(), Equals, "no install/refresh information results from the store") 2137 2138 op, name, err = e.SingleOpError() 2139 c.Check(op, Equals, "") 2140 c.Check(name, Equals, "") 2141 c.Check(err, IsNil) 2142 } 2143 2144 func (s *storeActionSuite) TestSnapActionRefreshesBothAuths(c *C) { 2145 // snap action (install/refresh) has is its own custom way to 2146 // signal macaroon refreshes that allows to do a best effort 2147 // with the available results 2148 2149 refresh, err := makeTestRefreshDischargeResponse() 2150 c.Assert(err, IsNil) 2151 c.Check(s.user.StoreDischarges[0], Not(Equals), refresh) 2152 2153 // mock refresh response 2154 refreshDischargeEndpointHit := false 2155 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2156 io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh)) 2157 refreshDischargeEndpointHit = true 2158 })) 2159 defer mockSSOServer.Close() 2160 store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh" 2161 2162 refreshSessionRequested := false 2163 expiredAuth := `Macaroon root="expired-session-macaroon"` 2164 n := 0 2165 // mock store response 2166 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2167 c.Check(r.UserAgent(), Equals, userAgent) 2168 2169 switch r.URL.Path { 2170 case snapActionPath: 2171 n++ 2172 type errObj struct { 2173 Code string `json:"code"` 2174 Message string `json:"message"` 2175 } 2176 var errors []errObj 2177 2178 authorization := r.Header.Get("Authorization") 2179 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 2180 if s.user.StoreDischarges[0] != refresh { 2181 errors = append(errors, errObj{Code: "user-authorization-needs-refresh"}) 2182 } 2183 2184 devAuthorization := r.Header.Get("Snap-Device-Authorization") 2185 if devAuthorization == "" { 2186 c.Fatalf("device authentication missing") 2187 } else if devAuthorization == expiredAuth { 2188 errors = append(errors, errObj{Code: "device-authorization-needs-refresh"}) 2189 } else { 2190 c.Check(devAuthorization, Equals, `Macaroon root="refreshed-session-macaroon"`) 2191 } 2192 2193 errorsJSON, err := json.Marshal(errors) 2194 c.Assert(err, IsNil) 2195 2196 io.WriteString(w, fmt.Sprintf(`{ 2197 "results": [{ 2198 "result": "refresh", 2199 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2200 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2201 "name": "hello-world", 2202 "snap": { 2203 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2204 "name": "hello-world", 2205 "revision": 26, 2206 "version": "6.1", 2207 "publisher": { 2208 "id": "canonical", 2209 "name": "canonical", 2210 "title": "Canonical" 2211 } 2212 } 2213 }], 2214 "error-list": %s 2215 }`, errorsJSON)) 2216 case authNoncesPath: 2217 io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) 2218 case authSessionPath: 2219 // sanity of request 2220 jsonReq, err := ioutil.ReadAll(r.Body) 2221 c.Assert(err, IsNil) 2222 var req map[string]string 2223 err = json.Unmarshal(jsonReq, &req) 2224 c.Assert(err, IsNil) 2225 c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) 2226 c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) 2227 c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) 2228 2229 authorization := r.Header.Get("X-Device-Authorization") 2230 if authorization == "" { 2231 c.Fatalf("expecting only refresh") 2232 } else { 2233 c.Check(authorization, Equals, expiredAuth) 2234 io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`) 2235 refreshSessionRequested = true 2236 } 2237 default: 2238 c.Fatalf("unexpected path %q", r.URL.Path) 2239 } 2240 })) 2241 c.Assert(mockServer, NotNil) 2242 defer mockServer.Close() 2243 2244 mockServerURL, _ := url.Parse(mockServer.URL) 2245 2246 // make sure device session is expired 2247 s.device.SessionMacaroon = "expired-session-macaroon" 2248 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 2249 sto := store.New(&store.Config{ 2250 StoreBaseURL: mockServerURL, 2251 }, dauthCtx) 2252 2253 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2254 { 2255 InstanceName: "hello-world", 2256 SnapID: helloWorldSnapID, 2257 TrackingChannel: "beta", 2258 Revision: snap.R(1), 2259 RefreshedDate: helloRefreshedDate, 2260 }, 2261 }, []*store.SnapAction{ 2262 { 2263 Action: "refresh", 2264 SnapID: helloWorldSnapID, 2265 InstanceName: "hello-world", 2266 }, 2267 }, nil, s.user, nil) 2268 c.Assert(err, IsNil) 2269 c.Assert(results, HasLen, 1) 2270 c.Assert(results[0].InstanceName(), Equals, "hello-world") 2271 c.Check(refreshDischargeEndpointHit, Equals, true) 2272 c.Check(refreshSessionRequested, Equals, true) 2273 c.Check(n, Equals, 2) 2274 } 2275 2276 func (s *storeActionSuite) TestSnapActionRefreshParallelInstall(c *C) { 2277 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2278 assertRequest(c, r, "POST", snapActionPath) 2279 // check device authorization is set, implicitly checking doRequest was used 2280 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2281 2282 jsonReq, err := ioutil.ReadAll(r.Body) 2283 c.Assert(err, IsNil) 2284 var req struct { 2285 Context []map[string]interface{} `json:"context"` 2286 Actions []map[string]interface{} `json:"actions"` 2287 } 2288 2289 err = json.Unmarshal(jsonReq, &req) 2290 c.Assert(err, IsNil) 2291 2292 c.Assert(req.Context, HasLen, 2) 2293 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2294 "snap-id": helloWorldSnapID, 2295 "instance-key": helloWorldSnapID, 2296 "revision": float64(26), 2297 "tracking-channel": "stable", 2298 "refreshed-date": helloRefreshedDateStr, 2299 "epoch": iZeroEpoch, 2300 }) 2301 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2302 "snap-id": helloWorldSnapID, 2303 "instance-key": helloWorldFooInstanceKeyWithSalt, 2304 "revision": float64(2), 2305 "tracking-channel": "stable", 2306 "refreshed-date": helloRefreshedDateStr, 2307 "epoch": iZeroEpoch, 2308 }) 2309 c.Assert(req.Actions, HasLen, 1) 2310 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2311 "action": "refresh", 2312 "instance-key": helloWorldFooInstanceKeyWithSalt, 2313 "snap-id": helloWorldSnapID, 2314 "channel": "stable", 2315 }) 2316 2317 io.WriteString(w, `{ 2318 "results": [{ 2319 "result": "refresh", 2320 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk", 2321 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2322 "name": "hello-world", 2323 "snap": { 2324 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2325 "name": "hello-world", 2326 "revision": 26, 2327 "version": "6.1", 2328 "publisher": { 2329 "id": "canonical", 2330 "username": "canonical", 2331 "display-name": "Canonical" 2332 } 2333 } 2334 }] 2335 }`) 2336 })) 2337 2338 c.Assert(mockServer, NotNil) 2339 defer mockServer.Close() 2340 2341 mockServerURL, _ := url.Parse(mockServer.URL) 2342 cfg := store.Config{ 2343 StoreBaseURL: mockServerURL, 2344 } 2345 dauthCtx := &testDauthContext{c: c, device: s.device} 2346 sto := store.New(&cfg, dauthCtx) 2347 2348 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2349 { 2350 InstanceName: "hello-world", 2351 SnapID: helloWorldSnapID, 2352 TrackingChannel: "stable", 2353 Revision: snap.R(26), 2354 RefreshedDate: helloRefreshedDate, 2355 }, { 2356 InstanceName: "hello-world_foo", 2357 SnapID: helloWorldSnapID, 2358 TrackingChannel: "stable", 2359 Revision: snap.R(2), 2360 RefreshedDate: helloRefreshedDate, 2361 }, 2362 }, []*store.SnapAction{ 2363 { 2364 Action: "refresh", 2365 SnapID: helloWorldSnapID, 2366 Channel: "stable", 2367 InstanceName: "hello-world_foo", 2368 }, 2369 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 2370 c.Assert(err, IsNil) 2371 c.Assert(results, HasLen, 1) 2372 c.Assert(results[0].SnapName(), Equals, "hello-world") 2373 c.Assert(results[0].InstanceName(), Equals, "hello-world_foo") 2374 c.Assert(results[0].Revision, Equals, snap.R(26)) 2375 } 2376 2377 func (s *storeActionSuite) TestSnapActionRefreshStableInstanceKey(c *C) { 2378 // salt "foo" 2379 helloWorldFooInstanceKeyWithSaltFoo := helloWorldSnapID + ":CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI" 2380 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2381 assertRequest(c, r, "POST", snapActionPath) 2382 // check device authorization is set, implicitly checking doRequest was used 2383 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2384 2385 jsonReq, err := ioutil.ReadAll(r.Body) 2386 c.Assert(err, IsNil) 2387 var req struct { 2388 Context []map[string]interface{} `json:"context"` 2389 Actions []map[string]interface{} `json:"actions"` 2390 } 2391 2392 err = json.Unmarshal(jsonReq, &req) 2393 c.Assert(err, IsNil) 2394 2395 c.Assert(req.Context, HasLen, 2) 2396 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2397 "snap-id": helloWorldSnapID, 2398 "instance-key": helloWorldSnapID, 2399 "revision": float64(26), 2400 "tracking-channel": "stable", 2401 "refreshed-date": helloRefreshedDateStr, 2402 "epoch": iZeroEpoch, 2403 "cohort-key": "what", 2404 }) 2405 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2406 "snap-id": helloWorldSnapID, 2407 "instance-key": helloWorldFooInstanceKeyWithSaltFoo, 2408 "revision": float64(2), 2409 "tracking-channel": "stable", 2410 "refreshed-date": helloRefreshedDateStr, 2411 "epoch": iZeroEpoch, 2412 }) 2413 c.Assert(req.Actions, HasLen, 1) 2414 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2415 "action": "refresh", 2416 "instance-key": helloWorldFooInstanceKeyWithSaltFoo, 2417 "snap-id": helloWorldSnapID, 2418 "channel": "stable", 2419 }) 2420 2421 io.WriteString(w, `{ 2422 "results": [{ 2423 "result": "refresh", 2424 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI", 2425 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2426 "name": "hello-world", 2427 "snap": { 2428 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2429 "name": "hello-world", 2430 "revision": 26, 2431 "version": "6.1", 2432 "publisher": { 2433 "id": "canonical", 2434 "username": "canonical", 2435 "display-name": "Canonical" 2436 } 2437 } 2438 }] 2439 }`) 2440 })) 2441 2442 c.Assert(mockServer, NotNil) 2443 defer mockServer.Close() 2444 2445 mockServerURL, _ := url.Parse(mockServer.URL) 2446 cfg := store.Config{ 2447 StoreBaseURL: mockServerURL, 2448 } 2449 dauthCtx := &testDauthContext{c: c, device: s.device} 2450 sto := store.New(&cfg, dauthCtx) 2451 2452 opts := &store.RefreshOptions{PrivacyKey: "foo"} 2453 currentSnaps := []*store.CurrentSnap{ 2454 { 2455 InstanceName: "hello-world", 2456 SnapID: helloWorldSnapID, 2457 TrackingChannel: "stable", 2458 Revision: snap.R(26), 2459 RefreshedDate: helloRefreshedDate, 2460 CohortKey: "what", 2461 }, { 2462 InstanceName: "hello-world_foo", 2463 SnapID: helloWorldSnapID, 2464 TrackingChannel: "stable", 2465 Revision: snap.R(2), 2466 RefreshedDate: helloRefreshedDate, 2467 }, 2468 } 2469 action := []*store.SnapAction{ 2470 { 2471 Action: "refresh", 2472 SnapID: helloWorldSnapID, 2473 Channel: "stable", 2474 InstanceName: "hello-world_foo", 2475 }, 2476 } 2477 results, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts) 2478 c.Assert(err, IsNil) 2479 c.Assert(results, HasLen, 1) 2480 c.Assert(results[0].SnapName(), Equals, "hello-world") 2481 c.Assert(results[0].InstanceName(), Equals, "hello-world_foo") 2482 c.Assert(results[0].Revision, Equals, snap.R(26)) 2483 2484 // another request with the same seed, gives same result 2485 resultsAgain, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts) 2486 c.Assert(err, IsNil) 2487 c.Assert(resultsAgain, DeepEquals, results) 2488 } 2489 2490 func (s *storeActionSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) { 2491 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2492 assertRequest(c, r, "POST", snapActionPath) 2493 // check device authorization is set, implicitly checking doRequest was used 2494 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2495 2496 jsonReq, err := ioutil.ReadAll(r.Body) 2497 c.Assert(err, IsNil) 2498 var req struct { 2499 Context []map[string]interface{} `json:"context"` 2500 Actions []map[string]interface{} `json:"actions"` 2501 } 2502 2503 err = json.Unmarshal(jsonReq, &req) 2504 c.Assert(err, IsNil) 2505 2506 c.Assert(req.Context, HasLen, 2) 2507 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2508 "snap-id": helloWorldSnapID, 2509 "instance-key": helloWorldSnapID, 2510 "revision": float64(26), 2511 "tracking-channel": "stable", 2512 "refreshed-date": helloRefreshedDateStr, 2513 "epoch": iZeroEpoch, 2514 }) 2515 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2516 "snap-id": helloWorldSnapID, 2517 "instance-key": helloWorldFooInstanceKeyWithSalt, 2518 "revision": float64(2), 2519 "tracking-channel": "edge", 2520 "refreshed-date": helloRefreshedDateStr, 2521 "epoch": iZeroEpoch, 2522 }) 2523 c.Assert(req.Actions, HasLen, 3) 2524 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2525 "action": "refresh", 2526 "instance-key": helloWorldSnapID, 2527 "snap-id": helloWorldSnapID, 2528 }) 2529 c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{ 2530 "action": "refresh", 2531 "instance-key": helloWorldFooInstanceKeyWithSalt, 2532 "snap-id": helloWorldSnapID, 2533 }) 2534 c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{ 2535 "action": "install", 2536 "instance-key": "install-1", 2537 "name": "other", 2538 "channel": "stable", 2539 "epoch": nil, 2540 }) 2541 2542 io.WriteString(w, `{ 2543 "results": [{ 2544 "result": "error", 2545 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2546 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2547 "name": "hello-world", 2548 "error": { 2549 "code": "revision-not-found", 2550 "message": "msg1" 2551 } 2552 }, { 2553 "result": "error", 2554 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk", 2555 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2556 "name": "hello-world", 2557 "error": { 2558 "code": "revision-not-found", 2559 "message": "msg2" 2560 } 2561 }, { 2562 "result": "error", 2563 "instance-key": "install-1", 2564 "snap-id": "foo-id", 2565 "name": "other", 2566 "error": { 2567 "code": "revision-not-found", 2568 "message": "msg3" 2569 } 2570 } 2571 ] 2572 }`) 2573 })) 2574 2575 c.Assert(mockServer, NotNil) 2576 defer mockServer.Close() 2577 2578 mockServerURL, _ := url.Parse(mockServer.URL) 2579 cfg := store.Config{ 2580 StoreBaseURL: mockServerURL, 2581 } 2582 dauthCtx := &testDauthContext{c: c, device: s.device} 2583 sto := store.New(&cfg, dauthCtx) 2584 2585 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2586 { 2587 InstanceName: "hello-world", 2588 SnapID: helloWorldSnapID, 2589 TrackingChannel: "stable", 2590 Revision: snap.R(26), 2591 RefreshedDate: helloRefreshedDate, 2592 }, 2593 { 2594 InstanceName: "hello-world_foo", 2595 SnapID: helloWorldSnapID, 2596 TrackingChannel: "edge", 2597 Revision: snap.R(2), 2598 RefreshedDate: helloRefreshedDate, 2599 }, 2600 }, []*store.SnapAction{ 2601 { 2602 Action: "refresh", 2603 InstanceName: "hello-world", 2604 SnapID: helloWorldSnapID, 2605 }, { 2606 Action: "refresh", 2607 InstanceName: "hello-world_foo", 2608 SnapID: helloWorldSnapID, 2609 }, { 2610 Action: "install", 2611 InstanceName: "other_foo", 2612 Channel: "stable", 2613 }, 2614 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 2615 c.Assert(results, HasLen, 0) 2616 c.Check(err, DeepEquals, &store.SnapActionError{ 2617 Refresh: map[string]error{ 2618 "hello-world": &store.RevisionNotAvailableError{ 2619 Action: "refresh", 2620 Channel: "stable", 2621 }, 2622 "hello-world_foo": &store.RevisionNotAvailableError{ 2623 Action: "refresh", 2624 Channel: "edge", 2625 }, 2626 }, 2627 Install: map[string]error{ 2628 "other_foo": &store.RevisionNotAvailableError{ 2629 Action: "install", 2630 Channel: "stable", 2631 }, 2632 }, 2633 }) 2634 } 2635 2636 func (s *storeActionSuite) TestSnapActionInstallParallelInstall(c *C) { 2637 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2638 assertRequest(c, r, "POST", snapActionPath) 2639 // check device authorization is set, implicitly checking doRequest was used 2640 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2641 2642 jsonReq, err := ioutil.ReadAll(r.Body) 2643 c.Assert(err, IsNil) 2644 var req struct { 2645 Context []map[string]interface{} `json:"context"` 2646 Actions []map[string]interface{} `json:"actions"` 2647 } 2648 2649 err = json.Unmarshal(jsonReq, &req) 2650 c.Assert(err, IsNil) 2651 2652 c.Assert(req.Context, HasLen, 1) 2653 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2654 "snap-id": helloWorldSnapID, 2655 "instance-key": helloWorldSnapID, 2656 "revision": float64(26), 2657 "tracking-channel": "stable", 2658 "refreshed-date": helloRefreshedDateStr, 2659 "epoch": iZeroEpoch, 2660 }) 2661 c.Assert(req.Actions, HasLen, 1) 2662 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2663 "action": "install", 2664 "instance-key": "install-1", 2665 "name": "hello-world", 2666 "channel": "stable", 2667 "epoch": nil, 2668 }) 2669 2670 io.WriteString(w, `{ 2671 "results": [{ 2672 "result": "install", 2673 "instance-key": "install-1", 2674 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2675 "name": "hello-world", 2676 "snap": { 2677 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2678 "name": "hello-world", 2679 "revision": 28, 2680 "version": "6.1", 2681 "publisher": { 2682 "id": "canonical", 2683 "username": "canonical", 2684 "display-name": "Canonical" 2685 } 2686 } 2687 }] 2688 }`) 2689 })) 2690 2691 c.Assert(mockServer, NotNil) 2692 defer mockServer.Close() 2693 2694 mockServerURL, _ := url.Parse(mockServer.URL) 2695 cfg := store.Config{ 2696 StoreBaseURL: mockServerURL, 2697 } 2698 dauthCtx := &testDauthContext{c: c, device: s.device} 2699 sto := store.New(&cfg, dauthCtx) 2700 2701 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2702 { 2703 InstanceName: "hello-world", 2704 SnapID: helloWorldSnapID, 2705 TrackingChannel: "stable", 2706 Revision: snap.R(26), 2707 RefreshedDate: helloRefreshedDate, 2708 }, 2709 }, []*store.SnapAction{ 2710 { 2711 Action: "install", 2712 InstanceName: "hello-world_foo", 2713 Channel: "stable", 2714 }, 2715 }, nil, nil, nil) 2716 c.Assert(err, IsNil) 2717 c.Assert(results, HasLen, 1) 2718 c.Assert(results[0].InstanceName(), Equals, "hello-world_foo") 2719 c.Assert(results[0].SnapName(), Equals, "hello-world") 2720 c.Assert(results[0].Revision, Equals, snap.R(28)) 2721 c.Assert(results[0].Version, Equals, "6.1") 2722 c.Assert(results[0].SnapID, Equals, helloWorldSnapID) 2723 c.Assert(results[0].Deltas, HasLen, 0) 2724 // effective-channel is not set 2725 c.Assert(results[0].Channel, Equals, "") 2726 } 2727 2728 func (s *storeActionSuite) TestSnapActionErrorsWhenNoInstanceName(c *C) { 2729 dauthCtx := &testDauthContext{c: c, device: s.device} 2730 sto := store.New(&store.Config{}, dauthCtx) 2731 2732 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2733 { 2734 InstanceName: "hello-world", 2735 SnapID: helloWorldSnapID, 2736 TrackingChannel: "stable", 2737 Revision: snap.R(26), 2738 RefreshedDate: helloRefreshedDate, 2739 }, 2740 }, []*store.SnapAction{ 2741 { 2742 Action: "install", 2743 Channel: "stable", 2744 }, 2745 }, nil, nil, nil) 2746 c.Assert(err, ErrorMatches, "internal error: action without instance name") 2747 c.Assert(results, IsNil) 2748 } 2749 2750 func (s *storeActionSuite) TestSnapActionInstallUnexpectedInstallKey(c *C) { 2751 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2752 assertRequest(c, r, "POST", snapActionPath) 2753 // check device authorization is set, implicitly checking doRequest was used 2754 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2755 2756 jsonReq, err := ioutil.ReadAll(r.Body) 2757 c.Assert(err, IsNil) 2758 var req struct { 2759 Context []map[string]interface{} `json:"context"` 2760 Actions []map[string]interface{} `json:"actions"` 2761 } 2762 2763 err = json.Unmarshal(jsonReq, &req) 2764 c.Assert(err, IsNil) 2765 2766 c.Assert(req.Context, HasLen, 1) 2767 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2768 "snap-id": helloWorldSnapID, 2769 "instance-key": helloWorldSnapID, 2770 "revision": float64(26), 2771 "tracking-channel": "stable", 2772 "refreshed-date": helloRefreshedDateStr, 2773 "epoch": iZeroEpoch, 2774 }) 2775 c.Assert(req.Actions, HasLen, 1) 2776 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2777 "action": "install", 2778 "instance-key": "install-1", 2779 "name": "hello-world", 2780 "channel": "stable", 2781 "epoch": nil, 2782 }) 2783 2784 io.WriteString(w, `{ 2785 "results": [{ 2786 "result": "install", 2787 "instance-key": "foo-2", 2788 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2789 "name": "hello-world", 2790 "snap": { 2791 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2792 "name": "hello-world", 2793 "revision": 28, 2794 "version": "6.1", 2795 "publisher": { 2796 "id": "canonical", 2797 "username": "canonical", 2798 "display-name": "Canonical" 2799 } 2800 } 2801 }] 2802 }`) 2803 })) 2804 2805 c.Assert(mockServer, NotNil) 2806 defer mockServer.Close() 2807 2808 mockServerURL, _ := url.Parse(mockServer.URL) 2809 cfg := store.Config{ 2810 StoreBaseURL: mockServerURL, 2811 } 2812 dauthCtx := &testDauthContext{c: c, device: s.device} 2813 sto := store.New(&cfg, dauthCtx) 2814 2815 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2816 { 2817 InstanceName: "hello-world", 2818 SnapID: helloWorldSnapID, 2819 TrackingChannel: "stable", 2820 Revision: snap.R(26), 2821 RefreshedDate: helloRefreshedDate, 2822 }, 2823 }, []*store.SnapAction{ 2824 { 2825 Action: "install", 2826 InstanceName: "hello-world_foo", 2827 Channel: "stable", 2828 }, 2829 }, nil, nil, nil) 2830 c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected instance-key "foo-2"`) 2831 c.Assert(results, IsNil) 2832 } 2833 2834 func (s *storeActionSuite) TestSnapActionRefreshUnexpectedInstanceKey(c *C) { 2835 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2836 assertRequest(c, r, "POST", snapActionPath) 2837 // check device authorization is set, implicitly checking doRequest was used 2838 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2839 2840 jsonReq, err := ioutil.ReadAll(r.Body) 2841 c.Assert(err, IsNil) 2842 var req struct { 2843 Context []map[string]interface{} `json:"context"` 2844 Actions []map[string]interface{} `json:"actions"` 2845 } 2846 2847 err = json.Unmarshal(jsonReq, &req) 2848 c.Assert(err, IsNil) 2849 2850 c.Assert(req.Context, HasLen, 1) 2851 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2852 "snap-id": helloWorldSnapID, 2853 "instance-key": helloWorldSnapID, 2854 "revision": float64(26), 2855 "tracking-channel": "stable", 2856 "refreshed-date": helloRefreshedDateStr, 2857 "epoch": iZeroEpoch, 2858 }) 2859 c.Assert(req.Actions, HasLen, 1) 2860 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2861 "action": "refresh", 2862 "instance-key": helloWorldSnapID, 2863 "snap-id": helloWorldSnapID, 2864 "channel": "stable", 2865 }) 2866 2867 io.WriteString(w, `{ 2868 "results": [{ 2869 "result": "refresh", 2870 "instance-key": "foo-5", 2871 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2872 "name": "hello-world", 2873 "snap": { 2874 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 2875 "name": "hello-world", 2876 "revision": 26, 2877 "version": "6.1", 2878 "publisher": { 2879 "id": "canonical", 2880 "username": "canonical", 2881 "display-name": "Canonical" 2882 } 2883 } 2884 }] 2885 }`) 2886 })) 2887 2888 c.Assert(mockServer, NotNil) 2889 defer mockServer.Close() 2890 2891 mockServerURL, _ := url.Parse(mockServer.URL) 2892 cfg := store.Config{ 2893 StoreBaseURL: mockServerURL, 2894 } 2895 dauthCtx := &testDauthContext{c: c, device: s.device} 2896 sto := store.New(&cfg, dauthCtx) 2897 2898 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 2899 { 2900 InstanceName: "hello-world", 2901 SnapID: helloWorldSnapID, 2902 TrackingChannel: "stable", 2903 Revision: snap.R(26), 2904 RefreshedDate: helloRefreshedDate, 2905 }, 2906 }, []*store.SnapAction{ 2907 { 2908 Action: "refresh", 2909 SnapID: helloWorldSnapID, 2910 Channel: "stable", 2911 InstanceName: "hello-world", 2912 }, 2913 }, nil, nil, nil) 2914 c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected refresh`) 2915 c.Assert(results, IsNil) 2916 } 2917 2918 func (s *storeActionSuite) TestSnapActionUnexpectedErrorKey(c *C) { 2919 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2920 assertRequest(c, r, "POST", snapActionPath) 2921 // check device authorization is set, implicitly checking doRequest was used 2922 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2923 2924 jsonReq, err := ioutil.ReadAll(r.Body) 2925 c.Assert(err, IsNil) 2926 var req struct { 2927 Context []map[string]interface{} `json:"context"` 2928 Actions []map[string]interface{} `json:"actions"` 2929 } 2930 2931 err = json.Unmarshal(jsonReq, &req) 2932 c.Assert(err, IsNil) 2933 2934 c.Assert(req.Context, HasLen, 2) 2935 c.Assert(req.Context[0], DeepEquals, map[string]interface{}{ 2936 "snap-id": helloWorldSnapID, 2937 "instance-key": helloWorldSnapID, 2938 "revision": float64(26), 2939 "tracking-channel": "stable", 2940 "refreshed-date": helloRefreshedDateStr, 2941 "epoch": iZeroEpoch, 2942 }) 2943 c.Assert(req.Context[1], DeepEquals, map[string]interface{}{ 2944 "snap-id": helloWorldSnapID, 2945 "instance-key": helloWorldFooInstanceKeyWithSalt, 2946 "revision": float64(2), 2947 "tracking-channel": "stable", 2948 "refreshed-date": helloRefreshedDateStr, 2949 "epoch": iZeroEpoch, 2950 }) 2951 c.Assert(req.Actions, HasLen, 1) 2952 c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{ 2953 "action": "install", 2954 "instance-key": "install-1", 2955 "name": "foo-2", 2956 "epoch": nil, 2957 }) 2958 2959 io.WriteString(w, `{ 2960 "results": [{ 2961 "result": "install", 2962 "instance-key": "install-1", 2963 "snap-id": "foo-2-id", 2964 "name": "foo-2", 2965 "snap": { 2966 "snap-id": "foo-2-id", 2967 "name": "foo-2", 2968 "revision": 28, 2969 "version": "6.1", 2970 "publisher": { 2971 "id": "canonical", 2972 "username": "canonical", 2973 "display-name": "Canonical" 2974 } 2975 } 2976 },{ 2977 "error": { 2978 "code": "duplicated-snap", 2979 "message": "The Snap is present more than once in the request." 2980 }, 2981 "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk", 2982 "name": null, 2983 "result": "error", 2984 "snap": null, 2985 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ" 2986 }] 2987 }`) 2988 })) 2989 2990 c.Assert(mockServer, NotNil) 2991 defer mockServer.Close() 2992 2993 mockServerURL, _ := url.Parse(mockServer.URL) 2994 cfg := store.Config{ 2995 StoreBaseURL: mockServerURL, 2996 } 2997 dauthCtx := &testDauthContext{c: c, device: s.device} 2998 sto := store.New(&cfg, dauthCtx) 2999 3000 results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{ 3001 { 3002 InstanceName: "hello-world", 3003 SnapID: helloWorldSnapID, 3004 TrackingChannel: "stable", 3005 Revision: snap.R(26), 3006 RefreshedDate: helloRefreshedDate, 3007 }, { 3008 InstanceName: "hello-world_foo", 3009 SnapID: helloWorldSnapID, 3010 TrackingChannel: "stable", 3011 Revision: snap.R(2), 3012 RefreshedDate: helloRefreshedDate, 3013 }, 3014 }, []*store.SnapAction{ 3015 { 3016 Action: "install", 3017 InstanceName: "foo-2", 3018 }, 3019 }, nil, nil, &store.RefreshOptions{PrivacyKey: "123"}) 3020 c.Assert(err, DeepEquals, &store.SnapActionError{ 3021 Other: []error{fmt.Errorf(`snap "hello-world_foo": The Snap is present more than once in the request.`)}, 3022 }) 3023 c.Assert(results, HasLen, 1) 3024 c.Assert(results[0].InstanceName(), Equals, "foo-2") 3025 c.Assert(results[0].SnapID, Equals, "foo-2-id") 3026 }