github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/store/store_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 "bytes" 24 "context" 25 "encoding/json" 26 "fmt" 27 "io" 28 "io/ioutil" 29 "net/http" 30 "net/http/httptest" 31 "net/url" 32 "os" 33 "regexp" 34 "sort" 35 "strings" 36 "sync" 37 "sync/atomic" 38 "testing" 39 "time" 40 41 . "gopkg.in/check.v1" 42 "gopkg.in/macaroon.v1" 43 "gopkg.in/retry.v1" 44 45 "github.com/snapcore/snapd/advisor" 46 "github.com/snapcore/snapd/arch" 47 "github.com/snapcore/snapd/asserts" 48 "github.com/snapcore/snapd/client" 49 "github.com/snapcore/snapd/dirs" 50 "github.com/snapcore/snapd/logger" 51 "github.com/snapcore/snapd/overlord/auth" 52 "github.com/snapcore/snapd/release" 53 "github.com/snapcore/snapd/snap" 54 "github.com/snapcore/snapd/snapdenv" 55 "github.com/snapcore/snapd/store" 56 "github.com/snapcore/snapd/testutil" 57 ) 58 59 func TestStore(t *testing.T) { TestingT(t) } 60 61 type configTestSuite struct{} 62 63 var _ = Suite(&configTestSuite{}) 64 65 var ( 66 // this is what snap.E("0") looks like when decoded into an interface{} (the /^i/ is for "interface") 67 iZeroEpoch = map[string]interface{}{ 68 "read": []interface{}{0.}, 69 "write": []interface{}{0.}, 70 } 71 // ...and this is snap.E("5*") 72 iFiveStarEpoch = map[string]interface{}{ 73 "read": []interface{}{4., 5.}, 74 "write": []interface{}{5.}, 75 } 76 ) 77 78 func (suite *configTestSuite) TestSetBaseURL(c *C) { 79 // Sanity check to prove at least one URI changes. 80 cfg := store.DefaultConfig() 81 c.Assert(cfg.StoreBaseURL.String(), Equals, "https://api.snapcraft.io/") 82 83 u, err := url.Parse("http://example.com/path/prefix/") 84 c.Assert(err, IsNil) 85 err = cfg.SetBaseURL(u) 86 c.Assert(err, IsNil) 87 88 c.Check(cfg.StoreBaseURL.String(), Equals, "http://example.com/path/prefix/") 89 c.Check(cfg.AssertionsBaseURL, IsNil) 90 } 91 92 func (suite *configTestSuite) TestSetBaseURLStoreOverrides(c *C) { 93 cfg := store.DefaultConfig() 94 c.Assert(cfg.SetBaseURL(store.ApiURL()), IsNil) 95 c.Check(cfg.StoreBaseURL, Matches, store.ApiURL().String()+".*") 96 97 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "https://force-api.local/"), IsNil) 98 defer os.Setenv("SNAPPY_FORCE_API_URL", "") 99 cfg = store.DefaultConfig() 100 c.Assert(cfg.SetBaseURL(store.ApiURL()), IsNil) 101 c.Check(cfg.StoreBaseURL.String(), Equals, "https://force-api.local/") 102 c.Check(cfg.AssertionsBaseURL, IsNil) 103 } 104 105 func (suite *configTestSuite) TestSetBaseURLStoreURLBadEnviron(c *C) { 106 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "://example.com"), IsNil) 107 defer os.Setenv("SNAPPY_FORCE_API_URL", "") 108 109 cfg := store.DefaultConfig() 110 err := cfg.SetBaseURL(store.ApiURL()) 111 c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_API_URL: parse \"?://example.com\"?: missing protocol scheme") 112 } 113 114 func (suite *configTestSuite) TestSetBaseURLAssertsOverrides(c *C) { 115 cfg := store.DefaultConfig() 116 c.Assert(cfg.SetBaseURL(store.ApiURL()), IsNil) 117 c.Check(cfg.AssertionsBaseURL, IsNil) 118 119 c.Assert(os.Setenv("SNAPPY_FORCE_SAS_URL", "https://force-sas.local/"), IsNil) 120 defer os.Setenv("SNAPPY_FORCE_SAS_URL", "") 121 cfg = store.DefaultConfig() 122 c.Assert(cfg.SetBaseURL(store.ApiURL()), IsNil) 123 c.Check(cfg.AssertionsBaseURL, Matches, "https://force-sas.local/.*") 124 } 125 126 func (suite *configTestSuite) TestSetBaseURLAssertsURLBadEnviron(c *C) { 127 c.Assert(os.Setenv("SNAPPY_FORCE_SAS_URL", "://example.com"), IsNil) 128 defer os.Setenv("SNAPPY_FORCE_SAS_URL", "") 129 130 cfg := store.DefaultConfig() 131 err := cfg.SetBaseURL(store.ApiURL()) 132 c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_SAS_URL: parse \"?://example.com\"?: missing protocol scheme") 133 } 134 135 const ( 136 // Store API paths/patterns. 137 authNoncesPath = "/api/v1/snaps/auth/nonces" 138 authSessionPath = "/api/v1/snaps/auth/sessions" 139 buyPath = "/api/v1/snaps/purchases/buy" 140 customersMePath = "/api/v1/snaps/purchases/customers/me" 141 detailsPathPattern = "/api/v1/snaps/details/.*" 142 ordersPath = "/api/v1/snaps/purchases/orders" 143 searchPath = "/api/v1/snaps/search" 144 sectionsPath = "/api/v1/snaps/sections" 145 // v2 146 findPath = "/v2/snaps/find" 147 snapActionPath = "/v2/snaps/refresh" 148 infoPathPattern = "/v2/snaps/info/.*" 149 cohortsPath = "/v2/cohorts" 150 ) 151 152 // Build details path for a snap name. 153 func detailsPath(snapName string) string { 154 return strings.Replace(detailsPathPattern, ".*", snapName, 1) 155 } 156 157 // Build info path for a snap name. 158 func infoPath(snapName string) string { 159 return strings.Replace(infoPathPattern, ".*", snapName, 1) 160 } 161 162 // Assert that a request is roughly as expected. Useful in fakes that should 163 // only attempt to handle a specific request. 164 func assertRequest(c *C, r *http.Request, method, pathPattern string) { 165 pathMatch, err := regexp.MatchString("^"+pathPattern+"$", r.URL.Path) 166 c.Assert(err, IsNil) 167 if r.Method != method || !pathMatch { 168 c.Fatalf("request didn't match (expected %s %s, got %s %s)", method, pathPattern, r.Method, r.URL.Path) 169 } 170 } 171 172 type baseStoreSuite struct { 173 testutil.BaseTest 174 175 device *auth.DeviceState 176 user *auth.UserState 177 178 ctx context.Context 179 180 logbuf *bytes.Buffer 181 } 182 183 const ( 184 exModel = `type: model 185 authority-id: my-brand 186 series: 16 187 brand-id: my-brand 188 model: baz-3000 189 architecture: armhf 190 gadget: gadget 191 kernel: kernel 192 store: my-brand-store-id 193 timestamp: 2016-08-20T13:00:00Z 194 sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij 195 196 AXNpZw=` 197 198 exSerial = `type: serial 199 authority-id: my-brand 200 brand-id: my-brand 201 model: baz-3000 202 serial: 9999 203 device-key: 204 AcbBTQRWhcGAARAAtJGIguK7FhSyRxL/6jvdy0zAgGCjC1xVNFzeF76p5G8BXNEEHZUHK+z8Gr2J 205 inVrpvhJhllf5Ob2dIMH2YQbC9jE1kjbzvuauQGDqk6tNQm0i3KDeHCSPgVN+PFXPwKIiLrh66Po 206 AC7OfR1rFUgCqu0jch0H6Nue0ynvEPiY4dPeXq7mCdpDr5QIAM41L+3hg0OdzvO8HMIGZQpdF6jP 207 7fkkVMROYvHUOJ8kknpKE7FiaNNpH7jK1qNxOYhLeiioX0LYrdmTvdTWHrSKZc82ZmlDjpKc4hUx 208 VtTXMAysw7CzIdREPom/vJklnKLvZt+Wk5AEF5V5YKnuT3pY+fjVMZ56GtTEeO/Er/oLk/n2xUK5 209 fD5DAyW/9z0ygzwTbY5IuWXyDfYneL4nXwWOEgg37Z4+8mTH+ftTz2dl1x1KIlIR2xo0kxf9t8K+ 210 jlr13vwF1+QReMCSUycUsZ2Eep5XhjI+LG7G1bMSGqodZTIOXLkIy6+3iJ8Z/feIHlJ0ELBDyFbl 211 Yy04Sf9LI148vJMsYenonkoWejWdMi8iCUTeaZydHJEUBU/RbNFLjCWa6NIUe9bfZgLiOOZkps54 212 +/AL078ri/tGjo/5UGvezSmwrEoWJyqrJt2M69N2oVDLJcHeo2bUYPtFC2Kfb2je58JrJ+llifdg 213 rAsxbnHXiXyVimUAEQEAAQ== 214 device-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu 215 timestamp: 2016-08-24T21:55:00Z 216 sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij 217 218 AXNpZw=` 219 220 exDeviceSessionRequest = `type: device-session-request 221 brand-id: my-brand 222 model: baz-3000 223 serial: 9999 224 nonce: @NONCE@ 225 timestamp: 2016-08-24T21:55:00Z 226 sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij 227 228 AXNpZw=` 229 ) 230 231 type testDauthContext struct { 232 c *C 233 device *auth.DeviceState 234 235 deviceMu sync.Mutex 236 deviceGetWitness func() 237 238 user *auth.UserState 239 240 proxyStoreID string 241 proxyStoreURL *url.URL 242 243 storeID string 244 245 cloudInfo *auth.CloudInfo 246 } 247 248 func (dac *testDauthContext) Device() (*auth.DeviceState, error) { 249 dac.deviceMu.Lock() 250 defer dac.deviceMu.Unlock() 251 freshDevice := auth.DeviceState{} 252 if dac.device != nil { 253 freshDevice = *dac.device 254 } 255 if dac.deviceGetWitness != nil { 256 dac.deviceGetWitness() 257 } 258 return &freshDevice, nil 259 } 260 261 func (dac *testDauthContext) UpdateDeviceAuth(d *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) { 262 dac.deviceMu.Lock() 263 defer dac.deviceMu.Unlock() 264 dac.c.Assert(d, DeepEquals, dac.device) 265 updated := *dac.device 266 updated.SessionMacaroon = newSessionMacaroon 267 *dac.device = updated 268 return &updated, nil 269 } 270 271 func (dac *testDauthContext) UpdateUserAuth(u *auth.UserState, newDischarges []string) (*auth.UserState, error) { 272 dac.c.Assert(u, DeepEquals, dac.user) 273 updated := *dac.user 274 updated.StoreDischarges = newDischarges 275 return &updated, nil 276 } 277 278 func (dac *testDauthContext) StoreID(fallback string) (string, error) { 279 if dac.storeID != "" { 280 return dac.storeID, nil 281 } 282 return fallback, nil 283 } 284 285 func (dac *testDauthContext) DeviceSessionRequestParams(nonce string) (*store.DeviceSessionRequestParams, error) { 286 model, err := asserts.Decode([]byte(exModel)) 287 if err != nil { 288 return nil, err 289 } 290 291 serial, err := asserts.Decode([]byte(exSerial)) 292 if err != nil { 293 return nil, err 294 } 295 296 sessReq, err := asserts.Decode([]byte(strings.Replace(exDeviceSessionRequest, "@NONCE@", nonce, 1))) 297 if err != nil { 298 return nil, err 299 } 300 301 return &store.DeviceSessionRequestParams{ 302 Request: sessReq.(*asserts.DeviceSessionRequest), 303 Serial: serial.(*asserts.Serial), 304 Model: model.(*asserts.Model), 305 }, nil 306 } 307 308 func (dac *testDauthContext) ProxyStoreParams(defaultURL *url.URL) (string, *url.URL, error) { 309 if dac.proxyStoreID != "" { 310 return dac.proxyStoreID, dac.proxyStoreURL, nil 311 } 312 return "", defaultURL, nil 313 } 314 315 func (dac *testDauthContext) CloudInfo() (*auth.CloudInfo, error) { 316 return dac.cloudInfo, nil 317 } 318 319 func makeTestMacaroon() (*macaroon.Macaroon, error) { 320 m, err := macaroon.New([]byte("secret"), "some-id", "location") 321 if err != nil { 322 return nil, err 323 } 324 err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", store.UbuntuoneLocation) 325 if err != nil { 326 return nil, err 327 } 328 329 return m, nil 330 } 331 332 func makeTestDischarge() (*macaroon.Macaroon, error) { 333 m, err := macaroon.New([]byte("shared-key"), "third-party-caveat", store.UbuntuoneLocation) 334 if err != nil { 335 return nil, err 336 } 337 338 return m, nil 339 } 340 341 func makeTestRefreshDischargeResponse() (string, error) { 342 m, err := macaroon.New([]byte("shared-key"), "refreshed-third-party-caveat", store.UbuntuoneLocation) 343 if err != nil { 344 return "", err 345 } 346 347 return auth.MacaroonSerialize(m) 348 } 349 350 func createTestUser(userID int, root, discharge *macaroon.Macaroon) (*auth.UserState, error) { 351 serializedMacaroon, err := auth.MacaroonSerialize(root) 352 if err != nil { 353 return nil, err 354 } 355 serializedDischarge, err := auth.MacaroonSerialize(discharge) 356 if err != nil { 357 return nil, err 358 } 359 360 return &auth.UserState{ 361 ID: userID, 362 Username: "test-user", 363 Macaroon: serializedMacaroon, 364 Discharges: []string{serializedDischarge}, 365 StoreMacaroon: serializedMacaroon, 366 StoreDischarges: []string{serializedDischarge}, 367 }, nil 368 } 369 370 func createTestDevice() *auth.DeviceState { 371 return &auth.DeviceState{ 372 Brand: "some-brand", 373 SessionMacaroon: "device-macaroon", 374 Serial: "9999", 375 } 376 } 377 378 func (s *baseStoreSuite) SetUpTest(c *C) { 379 s.BaseTest.SetUpTest(c) 380 s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) 381 382 dirs.SetRootDir(c.MkDir()) 383 s.AddCleanup(func() { dirs.SetRootDir("") }) 384 385 os.Setenv("SNAPD_DEBUG", "1") 386 s.AddCleanup(func() { os.Unsetenv("SNAPD_DEBUG") }) 387 388 var restoreLogger func() 389 s.logbuf, restoreLogger = logger.MockLogger() 390 s.AddCleanup(restoreLogger) 391 392 s.ctx = context.TODO() 393 394 s.device = createTestDevice() 395 396 root, err := makeTestMacaroon() 397 c.Assert(err, IsNil) 398 discharge, err := makeTestDischarge() 399 c.Assert(err, IsNil) 400 s.user, err = createTestUser(1, root, discharge) 401 c.Assert(err, IsNil) 402 403 store.MockDefaultRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second, 404 retry.Exponential{ 405 Initial: 1 * time.Millisecond, 406 Factor: 1, 407 }, 408 ))) 409 } 410 411 type storeTestSuite struct { 412 baseStoreSuite 413 } 414 415 var _ = Suite(&storeTestSuite{}) 416 417 func (s *storeTestSuite) SetUpTest(c *C) { 418 s.baseStoreSuite.SetUpTest(c) 419 } 420 421 func expectedAuthorization(c *C, user *auth.UserState) string { 422 var buf bytes.Buffer 423 424 root, err := auth.MacaroonDeserialize(user.StoreMacaroon) 425 c.Assert(err, IsNil) 426 discharge, err := auth.MacaroonDeserialize(user.StoreDischarges[0]) 427 c.Assert(err, IsNil) 428 discharge.Bind(root.Signature()) 429 430 serializedMacaroon, err := auth.MacaroonSerialize(root) 431 c.Assert(err, IsNil) 432 serializedDischarge, err := auth.MacaroonSerialize(discharge) 433 c.Assert(err, IsNil) 434 435 fmt.Fprintf(&buf, `Macaroon root="%s", discharge="%s"`, serializedMacaroon, serializedDischarge) 436 return buf.String() 437 } 438 439 var ( 440 userAgent = snapdenv.UserAgent() 441 ) 442 443 func (s *storeTestSuite) TestDoRequestSetsAuth(c *C) { 444 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 445 c.Check(r.UserAgent(), Equals, userAgent) 446 // check user authorization is set 447 authorization := r.Header.Get("Authorization") 448 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 449 // check device authorization is set 450 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 451 452 io.WriteString(w, "response-data") 453 })) 454 455 c.Assert(mockServer, NotNil) 456 defer mockServer.Close() 457 458 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 459 sto := store.New(&store.Config{}, dauthCtx) 460 461 endpoint, _ := url.Parse(mockServer.URL) 462 reqOptions := store.NewRequestOptions("GET", endpoint) 463 464 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 465 defer response.Body.Close() 466 c.Assert(err, IsNil) 467 468 responseData, err := ioutil.ReadAll(response.Body) 469 c.Assert(err, IsNil) 470 c.Check(string(responseData), Equals, "response-data") 471 } 472 473 func (s *storeTestSuite) TestDoRequestDoesNotSetAuthForLocalOnlyUser(c *C) { 474 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 475 c.Check(r.UserAgent(), Equals, userAgent) 476 // check no user authorization is set 477 authorization := r.Header.Get("Authorization") 478 c.Check(authorization, Equals, "") 479 // check device authorization is set 480 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 481 482 io.WriteString(w, "response-data") 483 })) 484 485 c.Assert(mockServer, NotNil) 486 defer mockServer.Close() 487 488 localUser := &auth.UserState{ 489 ID: 11, 490 Username: "test-user", 491 Macaroon: "snapd-macaroon", 492 } 493 494 dauthCtx := &testDauthContext{c: c, device: s.device, user: localUser} 495 sto := store.New(&store.Config{}, dauthCtx) 496 497 endpoint, _ := url.Parse(mockServer.URL) 498 reqOptions := store.NewRequestOptions("GET", endpoint) 499 500 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, localUser) 501 defer response.Body.Close() 502 c.Assert(err, IsNil) 503 504 responseData, err := ioutil.ReadAll(response.Body) 505 c.Assert(err, IsNil) 506 c.Check(string(responseData), Equals, "response-data") 507 } 508 509 func (s *storeTestSuite) TestDoRequestAuthNoSerial(c *C) { 510 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 511 c.Check(r.UserAgent(), Equals, userAgent) 512 // check user authorization is set 513 authorization := r.Header.Get("Authorization") 514 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 515 // check device authorization was not set 516 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 517 518 io.WriteString(w, "response-data") 519 })) 520 521 c.Assert(mockServer, NotNil) 522 defer mockServer.Close() 523 524 // no serial and no device macaroon => no device auth 525 s.device.Serial = "" 526 s.device.SessionMacaroon = "" 527 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 528 sto := store.New(&store.Config{}, dauthCtx) 529 530 endpoint, _ := url.Parse(mockServer.URL) 531 reqOptions := store.NewRequestOptions("GET", endpoint) 532 533 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 534 defer response.Body.Close() 535 c.Assert(err, IsNil) 536 537 responseData, err := ioutil.ReadAll(response.Body) 538 c.Assert(err, IsNil) 539 c.Check(string(responseData), Equals, "response-data") 540 } 541 542 func (s *storeTestSuite) TestDoRequestRefreshesAuth(c *C) { 543 refresh, err := makeTestRefreshDischargeResponse() 544 c.Assert(err, IsNil) 545 c.Check(s.user.StoreDischarges[0], Not(Equals), refresh) 546 547 // mock refresh response 548 refreshDischargeEndpointHit := false 549 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 550 io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh)) 551 refreshDischargeEndpointHit = true 552 })) 553 defer mockSSOServer.Close() 554 store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh" 555 556 // mock store response (requiring auth refresh) 557 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 558 c.Check(r.UserAgent(), Equals, userAgent) 559 560 authorization := r.Header.Get("Authorization") 561 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 562 if s.user.StoreDischarges[0] == refresh { 563 io.WriteString(w, "response-data") 564 } else { 565 w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1") 566 w.WriteHeader(401) 567 } 568 })) 569 c.Assert(mockServer, NotNil) 570 defer mockServer.Close() 571 572 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 573 sto := store.New(&store.Config{}, dauthCtx) 574 575 endpoint, _ := url.Parse(mockServer.URL) 576 reqOptions := store.NewRequestOptions("GET", endpoint) 577 578 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 579 defer response.Body.Close() 580 c.Assert(err, IsNil) 581 582 responseData, err := ioutil.ReadAll(response.Body) 583 c.Assert(err, IsNil) 584 c.Check(string(responseData), Equals, "response-data") 585 c.Check(refreshDischargeEndpointHit, Equals, true) 586 } 587 588 func (s *storeTestSuite) TestDoRequestForwardsRefreshAuthFailure(c *C) { 589 // mock refresh response 590 refreshDischargeEndpointHit := false 591 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 592 w.WriteHeader(mockStoreInvalidLoginCode) 593 io.WriteString(w, mockStoreInvalidLogin) 594 refreshDischargeEndpointHit = true 595 })) 596 defer mockSSOServer.Close() 597 store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh" 598 599 // mock store response (requiring auth refresh) 600 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 601 c.Check(r.UserAgent(), Equals, userAgent) 602 603 authorization := r.Header.Get("Authorization") 604 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 605 w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1") 606 w.WriteHeader(401) 607 })) 608 c.Assert(mockServer, NotNil) 609 defer mockServer.Close() 610 611 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 612 sto := store.New(&store.Config{}, dauthCtx) 613 614 endpoint, _ := url.Parse(mockServer.URL) 615 reqOptions := store.NewRequestOptions("GET", endpoint) 616 617 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 618 c.Assert(err, Equals, store.ErrInvalidCredentials) 619 c.Check(response, IsNil) 620 c.Check(refreshDischargeEndpointHit, Equals, true) 621 } 622 623 func (s *storeTestSuite) TestEnsureDeviceSession(c *C) { 624 deviceSessionRequested := 0 625 // mock store response 626 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 627 c.Check(r.UserAgent(), Equals, userAgent) 628 629 switch r.URL.Path { 630 case authNoncesPath: 631 io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) 632 case authSessionPath: 633 // sanity of request 634 jsonReq, err := ioutil.ReadAll(r.Body) 635 c.Assert(err, IsNil) 636 var req map[string]string 637 err = json.Unmarshal(jsonReq, &req) 638 c.Assert(err, IsNil) 639 c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) 640 c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) 641 c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) 642 authorization := r.Header.Get("X-Device-Authorization") 643 c.Assert(authorization, Equals, "") 644 deviceSessionRequested++ 645 io.WriteString(w, `{"macaroon": "fresh-session-macaroon"}`) 646 default: 647 c.Fatalf("unexpected path %q", r.URL.Path) 648 } 649 })) 650 c.Assert(mockServer, NotNil) 651 defer mockServer.Close() 652 653 mockServerURL, _ := url.Parse(mockServer.URL) 654 655 // make sure device session is not set 656 s.device.SessionMacaroon = "" 657 dauthCtx := &testDauthContext{c: c, device: s.device} 658 sto := store.New(&store.Config{ 659 StoreBaseURL: mockServerURL, 660 }, dauthCtx) 661 662 device, err := sto.EnsureDeviceSession() 663 c.Assert(err, IsNil) 664 665 c.Check(device.SessionMacaroon, Equals, "fresh-session-macaroon") 666 c.Check(s.device.SessionMacaroon, Equals, "fresh-session-macaroon") 667 c.Check(deviceSessionRequested, Equals, 1) 668 } 669 670 func (s *storeTestSuite) TestEnsureDeviceSessionSerialisation(c *C) { 671 var deviceSessionRequested int32 672 // mock store response 673 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 674 c.Check(r.UserAgent(), Equals, userAgent) 675 676 switch r.URL.Path { 677 case authNoncesPath: 678 io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) 679 case authSessionPath: 680 // sanity of request 681 jsonReq, err := ioutil.ReadAll(r.Body) 682 c.Assert(err, IsNil) 683 var req map[string]string 684 err = json.Unmarshal(jsonReq, &req) 685 c.Assert(err, IsNil) 686 c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) 687 c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) 688 c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) 689 authorization := r.Header.Get("X-Device-Authorization") 690 c.Assert(authorization, Equals, "") 691 atomic.AddInt32(&deviceSessionRequested, 1) 692 io.WriteString(w, `{"macaroon": "fresh-session-macaroon"}`) 693 default: 694 c.Fatalf("unexpected path %q", r.URL.Path) 695 } 696 })) 697 c.Assert(mockServer, NotNil) 698 defer mockServer.Close() 699 700 mockServerURL, _ := url.Parse(mockServer.URL) 701 702 wgGetDevice := new(sync.WaitGroup) 703 704 // make sure device session is not set 705 s.device.SessionMacaroon = "" 706 dauthCtx := &testDauthContext{ 707 c: c, 708 device: s.device, 709 deviceGetWitness: wgGetDevice.Done, 710 } 711 sto := store.New(&store.Config{ 712 StoreBaseURL: mockServerURL, 713 }, dauthCtx) 714 715 wg := new(sync.WaitGroup) 716 717 sto.SessionLock() 718 719 // try to acquire 10 times a device session in parallel; 720 // block these flows until all goroutines have acquired the original 721 // device state which is without a session, then let them run 722 for i := 0; i < 10; i++ { 723 wgGetDevice.Add(1) 724 wg.Add(1) 725 go func(n int) { 726 _, err := sto.EnsureDeviceSession() 727 c.Assert(err, IsNil) 728 wg.Done() 729 }(i) 730 } 731 732 wgGetDevice.Wait() 733 dauthCtx.deviceGetWitness = nil 734 // all flows have got the original device state 735 // let them run 736 sto.SessionUnlock() 737 // wait for the 10 flows to be done 738 wg.Wait() 739 740 c.Check(s.device.SessionMacaroon, Equals, "fresh-session-macaroon") 741 // we acquired a session from the store only once 742 c.Check(int(deviceSessionRequested), Equals, 1) 743 } 744 745 func (s *storeTestSuite) TestDoRequestSetsAndRefreshesDeviceAuth(c *C) { 746 deviceSessionRequested := false 747 refreshSessionRequested := false 748 expiredAuth := `Macaroon root="expired-session-macaroon"` 749 // mock store response 750 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 751 c.Check(r.UserAgent(), Equals, userAgent) 752 753 switch r.URL.Path { 754 case "/": 755 authorization := r.Header.Get("X-Device-Authorization") 756 if authorization == "" { 757 c.Fatalf("device authentication missing") 758 } else if authorization == expiredAuth { 759 w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1") 760 w.WriteHeader(401) 761 } else { 762 c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`) 763 io.WriteString(w, "response-data") 764 } 765 case authNoncesPath: 766 io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) 767 case authSessionPath: 768 // sanity of request 769 jsonReq, err := ioutil.ReadAll(r.Body) 770 c.Assert(err, IsNil) 771 var req map[string]string 772 err = json.Unmarshal(jsonReq, &req) 773 c.Assert(err, IsNil) 774 c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) 775 c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) 776 c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) 777 778 authorization := r.Header.Get("X-Device-Authorization") 779 if authorization == "" { 780 io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`) 781 deviceSessionRequested = true 782 } else { 783 c.Check(authorization, Equals, expiredAuth) 784 io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`) 785 refreshSessionRequested = true 786 } 787 default: 788 c.Fatalf("unexpected path %q", r.URL.Path) 789 } 790 })) 791 c.Assert(mockServer, NotNil) 792 defer mockServer.Close() 793 794 mockServerURL, _ := url.Parse(mockServer.URL) 795 796 // make sure device session is not set 797 s.device.SessionMacaroon = "" 798 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 799 sto := store.New(&store.Config{ 800 StoreBaseURL: mockServerURL, 801 }, dauthCtx) 802 803 reqOptions := store.NewRequestOptions("GET", mockServerURL) 804 805 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 806 c.Assert(err, IsNil) 807 defer response.Body.Close() 808 809 responseData, err := ioutil.ReadAll(response.Body) 810 c.Assert(err, IsNil) 811 c.Check(string(responseData), Equals, "response-data") 812 c.Check(deviceSessionRequested, Equals, true) 813 c.Check(refreshSessionRequested, Equals, true) 814 } 815 816 func (s *storeTestSuite) TestDoRequestSetsAndRefreshesBothAuths(c *C) { 817 refresh, err := makeTestRefreshDischargeResponse() 818 c.Assert(err, IsNil) 819 c.Check(s.user.StoreDischarges[0], Not(Equals), refresh) 820 821 // mock refresh response 822 refreshDischargeEndpointHit := false 823 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 824 io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh)) 825 refreshDischargeEndpointHit = true 826 })) 827 defer mockSSOServer.Close() 828 store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh" 829 830 refreshSessionRequested := false 831 expiredAuth := `Macaroon root="expired-session-macaroon"` 832 // mock store response 833 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 834 c.Check(r.UserAgent(), Equals, userAgent) 835 836 switch r.URL.Path { 837 case "/": 838 authorization := r.Header.Get("Authorization") 839 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 840 if s.user.StoreDischarges[0] != refresh { 841 w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1") 842 w.WriteHeader(401) 843 return 844 } 845 846 devAuthorization := r.Header.Get("X-Device-Authorization") 847 if devAuthorization == "" { 848 c.Fatalf("device authentication missing") 849 } else if devAuthorization == expiredAuth { 850 w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1") 851 w.WriteHeader(401) 852 } else { 853 c.Check(devAuthorization, Equals, `Macaroon root="refreshed-session-macaroon"`) 854 io.WriteString(w, "response-data") 855 } 856 case authNoncesPath: 857 io.WriteString(w, `{"nonce": "1234567890:9876543210"}`) 858 case authSessionPath: 859 // sanity of request 860 jsonReq, err := ioutil.ReadAll(r.Body) 861 c.Assert(err, IsNil) 862 var req map[string]string 863 err = json.Unmarshal(jsonReq, &req) 864 c.Assert(err, IsNil) 865 c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true) 866 c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true) 867 c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true) 868 869 authorization := r.Header.Get("X-Device-Authorization") 870 if authorization == "" { 871 c.Fatalf("expecting only refresh") 872 } else { 873 c.Check(authorization, Equals, expiredAuth) 874 io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`) 875 refreshSessionRequested = true 876 } 877 default: 878 c.Fatalf("unexpected path %q", r.URL.Path) 879 } 880 })) 881 c.Assert(mockServer, NotNil) 882 defer mockServer.Close() 883 884 mockServerURL, _ := url.Parse(mockServer.URL) 885 886 // make sure device session is expired 887 s.device.SessionMacaroon = "expired-session-macaroon" 888 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 889 sto := store.New(&store.Config{ 890 StoreBaseURL: mockServerURL, 891 }, dauthCtx) 892 893 reqOptions := store.NewRequestOptions("GET", mockServerURL) 894 895 resp, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 896 c.Assert(err, IsNil) 897 defer resp.Body.Close() 898 899 c.Check(resp.StatusCode, Equals, 200) 900 901 responseData, err := ioutil.ReadAll(resp.Body) 902 c.Assert(err, IsNil) 903 c.Check(string(responseData), Equals, "response-data") 904 c.Check(refreshDischargeEndpointHit, Equals, true) 905 c.Check(refreshSessionRequested, Equals, true) 906 } 907 908 func (s *storeTestSuite) TestDoRequestSetsExtraHeaders(c *C) { 909 // Custom headers are applied last. 910 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 911 c.Check(r.UserAgent(), Equals, `customAgent`) 912 c.Check(r.Header.Get("X-Foo-Header"), Equals, `Bar`) 913 c.Check(r.Header.Get("Content-Type"), Equals, `application/bson`) 914 c.Check(r.Header.Get("Accept"), Equals, `application/hal+bson`) 915 c.Check(r.Header.Get("Snap-Device-Capabilities"), Equals, "default-tracks") 916 io.WriteString(w, "response-data") 917 })) 918 c.Assert(mockServer, NotNil) 919 defer mockServer.Close() 920 921 sto := store.New(&store.Config{}, nil) 922 endpoint, _ := url.Parse(mockServer.URL) 923 reqOptions := store.NewRequestOptions("GET", endpoint) 924 reqOptions.ExtraHeaders = map[string]string{ 925 "X-Foo-Header": "Bar", 926 "Content-Type": "application/bson", 927 "Accept": "application/hal+bson", 928 "User-Agent": "customAgent", 929 } 930 931 response, err := sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 932 defer response.Body.Close() 933 c.Assert(err, IsNil) 934 935 responseData, err := ioutil.ReadAll(response.Body) 936 c.Assert(err, IsNil) 937 c.Check(string(responseData), Equals, "response-data") 938 } 939 940 func (s *storeTestSuite) TestLoginUser(c *C) { 941 macaroon, err := makeTestMacaroon() 942 c.Assert(err, IsNil) 943 serializedMacaroon, err := auth.MacaroonSerialize(macaroon) 944 c.Assert(err, IsNil) 945 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 946 w.WriteHeader(200) 947 io.WriteString(w, fmt.Sprintf(`{"macaroon": "%s"}`, serializedMacaroon)) 948 })) 949 c.Assert(mockServer, NotNil) 950 defer mockServer.Close() 951 store.MacaroonACLAPI = mockServer.URL + "/acl/" 952 953 discharge, err := makeTestDischarge() 954 c.Assert(err, IsNil) 955 serializedDischarge, err := auth.MacaroonSerialize(discharge) 956 c.Assert(err, IsNil) 957 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 958 w.WriteHeader(200) 959 io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, serializedDischarge)) 960 })) 961 c.Assert(mockSSOServer, NotNil) 962 defer mockSSOServer.Close() 963 store.UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge" 964 965 sto := store.New(nil, nil) 966 userMacaroon, userDischarge, err := sto.LoginUser("username", "password", "otp") 967 968 c.Assert(err, IsNil) 969 c.Check(userMacaroon, Equals, serializedMacaroon) 970 c.Check(userDischarge, Equals, serializedDischarge) 971 } 972 973 func (s *storeTestSuite) TestLoginUserDeveloperAPIError(c *C) { 974 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 975 w.WriteHeader(200) 976 io.WriteString(w, "{}") 977 })) 978 c.Assert(mockServer, NotNil) 979 defer mockServer.Close() 980 store.MacaroonACLAPI = mockServer.URL + "/acl/" 981 982 sto := store.New(nil, nil) 983 userMacaroon, userDischarge, err := sto.LoginUser("username", "password", "otp") 984 985 c.Assert(err, ErrorMatches, "cannot get snap access permission from store: .*") 986 c.Check(userMacaroon, Equals, "") 987 c.Check(userDischarge, Equals, "") 988 } 989 990 func (s *storeTestSuite) TestLoginUserSSOError(c *C) { 991 macaroon, err := makeTestMacaroon() 992 c.Assert(err, IsNil) 993 serializedMacaroon, err := auth.MacaroonSerialize(macaroon) 994 c.Assert(err, IsNil) 995 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 996 w.WriteHeader(200) 997 io.WriteString(w, fmt.Sprintf(`{"macaroon": "%s"}`, serializedMacaroon)) 998 })) 999 c.Assert(mockServer, NotNil) 1000 defer mockServer.Close() 1001 store.MacaroonACLAPI = mockServer.URL + "/acl/" 1002 1003 errorResponse := `{"code": "some-error"}` 1004 mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1005 w.WriteHeader(401) 1006 io.WriteString(w, errorResponse) 1007 })) 1008 c.Assert(mockSSOServer, NotNil) 1009 defer mockSSOServer.Close() 1010 store.UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge" 1011 1012 sto := store.New(nil, nil) 1013 userMacaroon, userDischarge, err := sto.LoginUser("username", "password", "otp") 1014 1015 c.Assert(err, ErrorMatches, "cannot authenticate to snap store: .*") 1016 c.Check(userMacaroon, Equals, "") 1017 c.Check(userDischarge, Equals, "") 1018 } 1019 1020 const ( 1021 funkyAppSnapID = "1e21e12ex4iim2xj1g2ul6f12f1" 1022 1023 helloWorldSnapID = "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ" 1024 // instance key used in refresh action of snap hello-world_foo, salt "123" 1025 helloWorldFooInstanceKeyWithSalt = helloWorldSnapID + ":IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk" 1026 helloWorldDeveloperID = "canonical" 1027 ) 1028 1029 const mockOrdersJSON = `{ 1030 "orders": [ 1031 { 1032 "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1033 "currency": "USD", 1034 "amount": "1.99", 1035 "state": "Complete", 1036 "refundable_until": "2015-07-15 18:46:21", 1037 "purchase_date": "2016-09-20T15:00:00+00:00" 1038 }, 1039 { 1040 "snap_id": "1e21e12ex4iim2xj1g2ul6f12f1", 1041 "currency": "USD", 1042 "amount": "1.99", 1043 "state": "Complete", 1044 "refundable_until": "2015-07-17 11:33:29", 1045 "purchase_date": "2016-09-20T15:00:00+00:00" 1046 } 1047 ] 1048 }` 1049 1050 const mockOrderResponseJSON = `{ 1051 "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1052 "currency": "USD", 1053 "amount": "1.99", 1054 "state": "Complete", 1055 "refundable_until": "2015-07-15 18:46:21", 1056 "purchase_date": "2016-09-20T15:00:00+00:00" 1057 }` 1058 1059 const mockSingleOrderJSON = `{ 1060 "orders": [ 1061 { 1062 "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1063 "currency": "USD", 1064 "amount": "1.99", 1065 "state": "Complete", 1066 "refundable_until": "2015-07-15 18:46:21", 1067 "purchase_date": "2016-09-20T15:00:00+00:00" 1068 } 1069 ] 1070 }` 1071 1072 /* acquired via 1073 1074 http --pretty=format --print b https://api.snapcraft.io/v2/snaps/info/hello-world architecture==amd64 fields==architectures,base,confinement,contact,created-at,description,download,epoch,license,name,prices,private,publisher,revision,snap-id,snap-yaml,summary,title,type,version,media,common-ids Snap-Device-Series:16 | xsel -b 1075 1076 on 2018-06-13 (note snap-yaml is currently excluded from that list). Then, by hand: 1077 - set prices to {"EUR": "0.99", "USD": "1.23"}, 1078 - set base in first channel-map entry to "bogus-base", 1079 - set snap-yaml in first channel-map entry to the one from the 'edge', plus the following pastiche: 1080 apps: 1081 content-plug: 1082 command: bin/content-plug 1083 plugs: [shared-content-plug] 1084 plugs: 1085 shared-content-plug: 1086 interface: content 1087 target: import 1088 content: mylib 1089 default-provider: test-snapd-content-slot 1090 slots: 1091 shared-content-slot: 1092 interface: content 1093 content: mylib 1094 read: 1095 - / 1096 1097 - add "released-at" to something randomish 1098 1099 */ 1100 const mockInfoJSON = `{ 1101 "channel-map": [ 1102 { 1103 "architectures": [ 1104 "all" 1105 ], 1106 "base": "bogus-base", 1107 "channel": { 1108 "architecture": "amd64", 1109 "name": "stable", 1110 "released-at": "2019-01-01T10:11:12.123456789+00:00", 1111 "risk": "stable", 1112 "track": "latest" 1113 }, 1114 "common-ids": [], 1115 "confinement": "strict", 1116 "created-at": "2016-07-12T16:37:23.960632+00:00", 1117 "download": { 1118 "deltas": [], 1119 "sha3-384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9", 1120 "size": 20480, 1121 "url": "https://api.snapcraft.io/api/v1/snaps/download/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap" 1122 }, 1123 "epoch": { 1124 "read": [ 1125 0 1126 ], 1127 "write": [ 1128 0 1129 ] 1130 }, 1131 "revision": 27, 1132 "snap-yaml": "name: hello-world\nversion: 6.3\narchitectures: [ all ]\nsummary: The 'hello-world' of snaps\ndescription: |\n This is a simple snap example that includes a few interesting binaries\n to demonstrate snaps and their confinement.\n * hello-world.env - dump the env of commands run inside app sandbox\n * hello-world.evil - show how snappy sandboxes binaries\n * hello-world.sh - enter interactive shell that runs in app sandbox\n * hello-world - simply output text\napps:\n env:\n command: bin/env\n evil:\n command: bin/evil\n sh:\n command: bin/sh\n hello-world:\n command: bin/echo\n content-plug:\n command: bin/content-plug\n plugs: [shared-content-plug]\nplugs:\n shared-content-plug:\n interface: content\n target: import\n content: mylib\n default-provider: test-snapd-content-slot\nslots:\n shared-content-slot:\n interface: content\n content: mylib\n read:\n - /\n", 1133 "type": "app", 1134 "version": "6.3" 1135 }, 1136 { 1137 "architectures": [ 1138 "all" 1139 ], 1140 "base": null, 1141 "channel": { 1142 "architecture": "amd64", 1143 "name": "candidate", 1144 "released-at": "2019-01-02T10:11:12.123456789+00:00", 1145 "risk": "candidate", 1146 "track": "latest" 1147 }, 1148 "common-ids": [], 1149 "confinement": "strict", 1150 "created-at": "2016-07-12T16:37:23.960632+00:00", 1151 "download": { 1152 "deltas": [], 1153 "sha3-384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9", 1154 "size": 20480, 1155 "url": "https://api.snapcraft.io/api/v1/snaps/download/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap" 1156 }, 1157 "epoch": { 1158 "read": [ 1159 0 1160 ], 1161 "write": [ 1162 0 1163 ] 1164 }, 1165 "revision": 27, 1166 "snap-yaml": "", 1167 "type": "app", 1168 "version": "6.3" 1169 }, 1170 { 1171 "architectures": [ 1172 "all" 1173 ], 1174 "base": null, 1175 "channel": { 1176 "architecture": "amd64", 1177 "name": "beta", 1178 "released-at": "2019-01-03T10:11:12.123456789+00:00", 1179 "risk": "beta", 1180 "track": "latest" 1181 }, 1182 "common-ids": [], 1183 "confinement": "strict", 1184 "created-at": "2016-07-12T16:37:23.960632+00:00", 1185 "download": { 1186 "deltas": [], 1187 "sha3-384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9", 1188 "size": 20480, 1189 "url": "https://api.snapcraft.io/api/v1/snaps/download/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap" 1190 }, 1191 "epoch": { 1192 "read": [ 1193 0 1194 ], 1195 "write": [ 1196 0 1197 ] 1198 }, 1199 "revision": 27, 1200 "snap-yaml": "", 1201 "type": "app", 1202 "version": "6.3" 1203 }, 1204 { 1205 "architectures": [ 1206 "all" 1207 ], 1208 "base": null, 1209 "channel": { 1210 "architecture": "amd64", 1211 "name": "edge", 1212 "released-at": "2019-01-04T10:11:12.123456789+00:00", 1213 "risk": "edge", 1214 "track": "latest" 1215 }, 1216 "common-ids": [], 1217 "confinement": "strict", 1218 "created-at": "2017-11-20T07:59:46.563940+00:00", 1219 "download": { 1220 "deltas": [], 1221 "sha3-384": "d888ed75a9071ace39fed922aa799cad4081de79fda650fbbf75e1bae780dae2c24a19aab8db5059c6ad0d0533d90c04", 1222 "size": 20480, 1223 "url": "https://api.snapcraft.io/api/v1/snaps/download/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_28.snap" 1224 }, 1225 "epoch": { 1226 "read": [ 1227 0 1228 ], 1229 "write": [ 1230 0 1231 ] 1232 }, 1233 "revision": 28, 1234 "snap-yaml": "", 1235 "type": "app", 1236 "version": "6.3" 1237 } 1238 ], 1239 "name": "hello-world", 1240 "snap": { 1241 "contact": "mailto:snappy-devel@lists.ubuntu.com", 1242 "description": "This is a simple hello world example.", 1243 "license": "MIT", 1244 "media": [ 1245 { 1246 "height": null, 1247 "type": "icon", 1248 "url": "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", 1249 "width": null 1250 }, 1251 { 1252 "height": null, 1253 "type": "screenshot", 1254 "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png", 1255 "width": null 1256 } 1257 ], 1258 "name": "hello-world", 1259 "prices": {"EUR": "0.99", "USD": "1.23"}, 1260 "private": true, 1261 "publisher": { 1262 "display-name": "Canonical", 1263 "id": "canonical", 1264 "username": "canonical", 1265 "validation": "verified" 1266 }, 1267 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1268 "summary": "The 'hello-world' of snaps", 1269 "title": "Hello World" 1270 }, 1271 "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ" 1272 }` 1273 1274 func (s *storeTestSuite) TestInfo(c *C) { 1275 restore := release.MockOnClassic(false) 1276 defer restore() 1277 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1278 assertRequest(c, r, "GET", infoPathPattern) 1279 c.Check(r.UserAgent(), Equals, userAgent) 1280 1281 // check device authorization is set, implicitly checking doRequest was used 1282 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1283 1284 // no store ID by default 1285 storeID := r.Header.Get("Snap-Device-Store") 1286 c.Check(storeID, Equals, "") 1287 1288 c.Check(r.URL.Path, Matches, ".*/hello-world") 1289 1290 query := r.URL.Query() 1291 c.Check(query.Get("fields"), Equals, "abc,def") 1292 c.Check(query.Get("architecture"), Equals, arch.DpkgArchitecture()) 1293 1294 w.Header().Set("X-Suggested-Currency", "GBP") 1295 w.WriteHeader(200) 1296 io.WriteString(w, mockInfoJSON) 1297 })) 1298 1299 c.Assert(mockServer, NotNil) 1300 defer mockServer.Close() 1301 1302 mockServerURL, _ := url.Parse(mockServer.URL) 1303 cfg := store.Config{ 1304 StoreBaseURL: mockServerURL, 1305 InfoFields: []string{"abc", "def"}, 1306 } 1307 dauthCtx := &testDauthContext{c: c, device: s.device} 1308 sto := store.New(&cfg, dauthCtx) 1309 1310 // the actual test 1311 spec := store.SnapSpec{ 1312 Name: "hello-world", 1313 } 1314 result, err := sto.SnapInfo(s.ctx, spec, nil) 1315 c.Assert(err, IsNil) 1316 c.Check(result.InstanceName(), Equals, "hello-world") 1317 c.Check(result.Architectures, DeepEquals, []string{"all"}) 1318 c.Check(result.Revision, Equals, snap.R(27)) 1319 c.Check(result.SnapID, Equals, helloWorldSnapID) 1320 c.Check(result.Publisher, Equals, snap.StoreAccount{ 1321 ID: "canonical", 1322 Username: "canonical", 1323 DisplayName: "Canonical", 1324 Validation: "verified", 1325 }) 1326 c.Check(result.Version, Equals, "6.3") 1327 c.Check(result.Sha3_384, Matches, `[[:xdigit:]]{96}`) 1328 c.Check(result.Size, Equals, int64(20480)) 1329 c.Check(result.Channel, Equals, "stable") 1330 c.Check(result.Description(), Equals, "This is a simple hello world example.") 1331 c.Check(result.Summary(), Equals, "The 'hello-world' of snaps") 1332 c.Check(result.Title(), Equals, "Hello World") // TODO: have this updated to be different to the name 1333 c.Check(result.License, Equals, "MIT") 1334 c.Check(result.Prices, DeepEquals, map[string]float64{"EUR": 0.99, "USD": 1.23}) 1335 c.Check(result.Paid, Equals, true) 1336 c.Check(result.Media, DeepEquals, snap.MediaInfos{ 1337 { 1338 Type: "icon", 1339 URL: "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", 1340 }, { 1341 Type: "screenshot", 1342 URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png", 1343 }, 1344 }) 1345 c.Check(result.MustBuy, Equals, true) 1346 c.Check(result.Contact, Equals, "mailto:snappy-devel@lists.ubuntu.com") 1347 c.Check(result.Base, Equals, "bogus-base") 1348 c.Check(result.Epoch.String(), Equals, "0") 1349 c.Check(sto.SuggestedCurrency(), Equals, "GBP") 1350 c.Check(result.Private, Equals, true) 1351 1352 c.Check(snap.Validate(result), IsNil) 1353 1354 // validate the plugs/slots (only here because we faked stuff in the JSON) 1355 c.Assert(result.Plugs, HasLen, 1) 1356 plug := result.Plugs["shared-content-plug"] 1357 c.Check(plug.Name, Equals, "shared-content-plug") 1358 c.Check(plug.Snap, DeepEquals, result) 1359 c.Check(plug.Apps, HasLen, 1) 1360 c.Check(plug.Apps["content-plug"].Command, Equals, "bin/content-plug") 1361 1362 c.Assert(result.Slots, HasLen, 1) 1363 slot := result.Slots["shared-content-slot"] 1364 c.Check(slot.Name, Equals, "shared-content-slot") 1365 c.Check(slot.Snap, DeepEquals, result) 1366 c.Check(slot.Apps, HasLen, 5) 1367 c.Check(slot.Apps["content-plug"].Command, Equals, "bin/content-plug") 1368 } 1369 1370 func (s *storeTestSuite) TestInfoBadResponses(c *C) { 1371 restore := release.MockOnClassic(false) 1372 defer restore() 1373 n := 0 1374 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1375 n++ 1376 switch n { 1377 case 1: 1378 // This one should work. 1379 // (strictly speaking the channel map item should at least have a "channel" member) 1380 io.WriteString(w, `{"channel-map": [{}], "snap": {"name":"hello"}}`) 1381 case 2: 1382 // "not found" (no channel map) 1383 io.WriteString(w, `{"snap":{"name":"hello"}}`) 1384 case 3: 1385 // "not found" (same) 1386 io.WriteString(w, `{"channel-map": [], "snap": {"name":"hello"}}`) 1387 case 4: 1388 // bad price 1389 io.WriteString(w, `{"channel-map": [{}], "snap": {"name":"hello","prices":{"XPD": "Palladium?!?"}}}`) 1390 default: 1391 c.Errorf("expected at most 4 calls, now on #%d", n) 1392 } 1393 })) 1394 c.Assert(mockServer, NotNil) 1395 defer mockServer.Close() 1396 1397 mockServerURL, _ := url.Parse(mockServer.URL) 1398 cfg := store.Config{ 1399 StoreBaseURL: mockServerURL, 1400 InfoFields: []string{}, 1401 } 1402 dauthCtx := &testDauthContext{c: c, device: s.device} 1403 sto := store.New(&cfg, dauthCtx) 1404 1405 info, err := sto.SnapInfo(s.ctx, store.SnapSpec{Name: "hello"}, nil) 1406 c.Assert(err, IsNil) 1407 c.Check(info.InstanceName(), Equals, "hello") 1408 1409 info, err = sto.SnapInfo(s.ctx, store.SnapSpec{Name: "hello"}, nil) 1410 c.Check(err, Equals, store.ErrSnapNotFound) 1411 c.Check(info, IsNil) 1412 1413 info, err = sto.SnapInfo(s.ctx, store.SnapSpec{Name: "hello"}, nil) 1414 c.Check(err, Equals, store.ErrSnapNotFound) 1415 c.Check(info, IsNil) 1416 1417 info, err = sto.SnapInfo(s.ctx, store.SnapSpec{Name: "hello"}, nil) 1418 c.Check(err, ErrorMatches, `.* invalid syntax`) 1419 c.Check(info, IsNil) 1420 } 1421 1422 func (s *storeTestSuite) TestInfoDefaultChannelIsStable(c *C) { 1423 restore := release.MockOnClassic(false) 1424 defer restore() 1425 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1426 assertRequest(c, r, "GET", infoPathPattern) 1427 c.Check(r.URL.Path, Matches, ".*/hello-world") 1428 1429 w.WriteHeader(200) 1430 1431 io.WriteString(w, mockInfoJSON) 1432 })) 1433 1434 c.Assert(mockServer, NotNil) 1435 defer mockServer.Close() 1436 1437 mockServerURL, _ := url.Parse(mockServer.URL) 1438 cfg := store.Config{ 1439 StoreBaseURL: mockServerURL, 1440 DetailFields: []string{"abc", "def"}, 1441 } 1442 dauthCtx := &testDauthContext{c: c, device: s.device} 1443 sto := store.New(&cfg, dauthCtx) 1444 1445 // the actual test 1446 spec := store.SnapSpec{ 1447 Name: "hello-world", 1448 } 1449 result, err := sto.SnapInfo(s.ctx, spec, nil) 1450 c.Assert(err, IsNil) 1451 c.Check(result.InstanceName(), Equals, "hello-world") 1452 c.Check(result.SnapID, Equals, helloWorldSnapID) 1453 c.Check(result.Channel, Equals, "stable") 1454 } 1455 1456 func (s *storeTestSuite) TestInfo500(c *C) { 1457 var n = 0 1458 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1459 assertRequest(c, r, "GET", infoPathPattern) 1460 n++ 1461 w.WriteHeader(500) 1462 })) 1463 1464 c.Assert(mockServer, NotNil) 1465 defer mockServer.Close() 1466 1467 mockServerURL, _ := url.Parse(mockServer.URL) 1468 cfg := store.Config{ 1469 StoreBaseURL: mockServerURL, 1470 DetailFields: []string{}, 1471 } 1472 dauthCtx := &testDauthContext{c: c, device: s.device} 1473 sto := store.New(&cfg, dauthCtx) 1474 1475 // the actual test 1476 spec := store.SnapSpec{ 1477 Name: "hello-world", 1478 } 1479 _, err := sto.SnapInfo(s.ctx, spec, nil) 1480 c.Assert(err, NotNil) 1481 c.Assert(err, ErrorMatches, `cannot get details for snap "hello-world": got unexpected HTTP status code 500 via GET to "http://.*?/info/hello-world.*"`) 1482 c.Assert(n, Equals, 5) 1483 } 1484 1485 func (s *storeTestSuite) TestInfo500Once(c *C) { 1486 var n = 0 1487 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1488 assertRequest(c, r, "GET", infoPathPattern) 1489 n++ 1490 if n > 1 { 1491 w.Header().Set("X-Suggested-Currency", "GBP") 1492 w.WriteHeader(200) 1493 io.WriteString(w, mockInfoJSON) 1494 } else { 1495 w.WriteHeader(500) 1496 } 1497 })) 1498 1499 c.Assert(mockServer, NotNil) 1500 defer mockServer.Close() 1501 1502 mockServerURL, _ := url.Parse(mockServer.URL) 1503 cfg := store.Config{ 1504 StoreBaseURL: mockServerURL, 1505 } 1506 dauthCtx := &testDauthContext{c: c, device: s.device} 1507 sto := store.New(&cfg, dauthCtx) 1508 1509 // the actual test 1510 spec := store.SnapSpec{ 1511 Name: "hello-world", 1512 } 1513 result, err := sto.SnapInfo(s.ctx, spec, nil) 1514 c.Assert(err, IsNil) 1515 c.Check(result.InstanceName(), Equals, "hello-world") 1516 c.Assert(n, Equals, 2) 1517 } 1518 1519 func (s *storeTestSuite) TestInfoAndChannels(c *C) { 1520 n := 0 1521 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1522 assertRequest(c, r, "GET", infoPathPattern) 1523 switch n { 1524 case 0: 1525 c.Check(r.URL.Path, Matches, ".*/hello-world") 1526 1527 w.Header().Set("X-Suggested-Currency", "GBP") 1528 w.WriteHeader(200) 1529 1530 io.WriteString(w, mockInfoJSON) 1531 default: 1532 c.Fatalf("unexpected request to %q", r.URL.Path) 1533 } 1534 n++ 1535 })) 1536 1537 c.Assert(mockServer, NotNil) 1538 defer mockServer.Close() 1539 1540 mockServerURL, _ := url.Parse(mockServer.URL) 1541 cfg := store.Config{ 1542 StoreBaseURL: mockServerURL, 1543 } 1544 dauthCtx := &testDauthContext{c: c, device: s.device} 1545 sto := store.New(&cfg, dauthCtx) 1546 1547 // the actual test 1548 spec := store.SnapSpec{ 1549 Name: "hello-world", 1550 } 1551 result, err := sto.SnapInfo(s.ctx, spec, nil) 1552 c.Assert(err, IsNil) 1553 c.Assert(n, Equals, 1) 1554 c.Check(result.InstanceName(), Equals, "hello-world") 1555 expected := map[string]*snap.ChannelSnapInfo{ 1556 "latest/stable": { 1557 Revision: snap.R(27), 1558 Version: "6.3", 1559 Confinement: snap.StrictConfinement, 1560 Channel: "latest/stable", 1561 Size: 20480, 1562 Epoch: snap.E("0"), 1563 ReleasedAt: time.Date(2019, 1, 1, 10, 11, 12, 123456789, time.UTC), 1564 }, 1565 "latest/candidate": { 1566 Revision: snap.R(27), 1567 Version: "6.3", 1568 Confinement: snap.StrictConfinement, 1569 Channel: "latest/candidate", 1570 Size: 20480, 1571 Epoch: snap.E("0"), 1572 ReleasedAt: time.Date(2019, 1, 2, 10, 11, 12, 123456789, time.UTC), 1573 }, 1574 "latest/beta": { 1575 Revision: snap.R(27), 1576 Version: "6.3", 1577 Confinement: snap.StrictConfinement, 1578 Channel: "latest/beta", 1579 Size: 20480, 1580 Epoch: snap.E("0"), 1581 ReleasedAt: time.Date(2019, 1, 3, 10, 11, 12, 123456789, time.UTC), 1582 }, 1583 "latest/edge": { 1584 Revision: snap.R(28), 1585 Version: "6.3", 1586 Confinement: snap.StrictConfinement, 1587 Channel: "latest/edge", 1588 Size: 20480, 1589 Epoch: snap.E("0"), 1590 ReleasedAt: time.Date(2019, 1, 4, 10, 11, 12, 123456789, time.UTC), 1591 }, 1592 } 1593 for k, v := range result.Channels { 1594 c.Check(v, DeepEquals, expected[k], Commentf("%q", k)) 1595 } 1596 c.Check(result.Channels, HasLen, len(expected)) 1597 1598 c.Check(snap.Validate(result), IsNil) 1599 } 1600 1601 func (s *storeTestSuite) TestInfoMoreChannels(c *C) { 1602 // NB this tests more channels, but still only one architecture 1603 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1604 assertRequest(c, r, "GET", infoPathPattern) 1605 // following is just an aligned version of: 1606 // http https://api.snapcraft.io/v2/snaps/info/go architecture==amd64 fields==channel Snap-Device-Series:16 | jq -c '.["channel-map"] | .[]' 1607 io.WriteString(w, `{"channel-map": [ 1608 {"channel":{"architecture":"amd64","name":"stable", "released-at":"2018-12-17T09:17:16.288554+00:00","risk":"stable", "track":"latest"}}, 1609 {"channel":{"architecture":"amd64","name":"edge", "released-at":"2018-11-06T00:46:03.348730+00:00","risk":"edge", "track":"latest"}}, 1610 {"channel":{"architecture":"amd64","name":"1.11/stable", "released-at":"2018-12-17T09:17:48.847205+00:00","risk":"stable", "track":"1.11"}}, 1611 {"channel":{"architecture":"amd64","name":"1.11/candidate","released-at":"2018-12-17T00:10:05.864910+00:00","risk":"candidate","track":"1.11"}}, 1612 {"channel":{"architecture":"amd64","name":"1.10/stable", "released-at":"2018-12-17T06:53:57.915517+00:00","risk":"stable", "track":"1.10"}}, 1613 {"channel":{"architecture":"amd64","name":"1.10/candidate","released-at":"2018-12-17T00:04:13.413244+00:00","risk":"candidate","track":"1.10"}}, 1614 {"channel":{"architecture":"amd64","name":"1.9/stable", "released-at":"2018-06-13T02:23:06.338145+00:00","risk":"stable", "track":"1.9"}}, 1615 {"channel":{"architecture":"amd64","name":"1.8/stable", "released-at":"2018-02-07T23:08:59.152984+00:00","risk":"stable", "track":"1.8"}}, 1616 {"channel":{"architecture":"amd64","name":"1.7/stable", "released-at":"2017-06-02T01:16:52.640258+00:00","risk":"stable", "track":"1.7"}}, 1617 {"channel":{"architecture":"amd64","name":"1.6/stable", "released-at":"2017-05-17T21:18:42.224979+00:00","risk":"stable", "track":"1.6"}} 1618 ]}`) 1619 })) 1620 1621 c.Assert(mockServer, NotNil) 1622 defer mockServer.Close() 1623 1624 mockServerURL, _ := url.Parse(mockServer.URL) 1625 cfg := store.Config{ 1626 StoreBaseURL: mockServerURL, 1627 } 1628 dauthCtx := &testDauthContext{c: c, device: s.device} 1629 sto := store.New(&cfg, dauthCtx) 1630 1631 // the actual test 1632 result, err := sto.SnapInfo(s.ctx, store.SnapSpec{Name: "eh"}, nil) 1633 c.Assert(err, IsNil) 1634 expected := map[string]*snap.ChannelSnapInfo{ 1635 "latest/stable": {Channel: "latest/stable", ReleasedAt: time.Date(2018, 12, 17, 9, 17, 16, 288554000, time.UTC)}, 1636 "latest/edge": {Channel: "latest/edge", ReleasedAt: time.Date(2018, 11, 6, 0, 46, 3, 348730000, time.UTC)}, 1637 "1.6/stable": {Channel: "1.6/stable", ReleasedAt: time.Date(2017, 5, 17, 21, 18, 42, 224979000, time.UTC)}, 1638 "1.7/stable": {Channel: "1.7/stable", ReleasedAt: time.Date(2017, 6, 2, 1, 16, 52, 640258000, time.UTC)}, 1639 "1.8/stable": {Channel: "1.8/stable", ReleasedAt: time.Date(2018, 2, 7, 23, 8, 59, 152984000, time.UTC)}, 1640 "1.9/stable": {Channel: "1.9/stable", ReleasedAt: time.Date(2018, 6, 13, 2, 23, 6, 338145000, time.UTC)}, 1641 "1.10/stable": {Channel: "1.10/stable", ReleasedAt: time.Date(2018, 12, 17, 6, 53, 57, 915517000, time.UTC)}, 1642 "1.10/candidate": {Channel: "1.10/candidate", ReleasedAt: time.Date(2018, 12, 17, 0, 4, 13, 413244000, time.UTC)}, 1643 "1.11/stable": {Channel: "1.11/stable", ReleasedAt: time.Date(2018, 12, 17, 9, 17, 48, 847205000, time.UTC)}, 1644 "1.11/candidate": {Channel: "1.11/candidate", ReleasedAt: time.Date(2018, 12, 17, 0, 10, 5, 864910000, time.UTC)}, 1645 } 1646 for k, v := range result.Channels { 1647 c.Check(v, DeepEquals, expected[k], Commentf("%q", k)) 1648 } 1649 c.Check(result.Channels, HasLen, len(expected)) 1650 c.Check(result.Tracks, DeepEquals, []string{"latest", "1.11", "1.10", "1.9", "1.8", "1.7", "1.6"}) 1651 } 1652 1653 func (s *storeTestSuite) TestInfoNonDefaults(c *C) { 1654 restore := release.MockOnClassic(true) 1655 defer restore() 1656 1657 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1658 assertRequest(c, r, "GET", infoPathPattern) 1659 c.Check(r.Header.Get("Snap-Device-Store"), Equals, "foo") 1660 c.Check(r.URL.Path, Matches, ".*/hello-world$") 1661 1662 c.Check(r.Header.Get("Snap-Device-Series"), Equals, "21") 1663 c.Check(r.URL.Query().Get("architecture"), Equals, "archXYZ") 1664 1665 w.WriteHeader(200) 1666 io.WriteString(w, mockInfoJSON) 1667 })) 1668 1669 c.Assert(mockServer, NotNil) 1670 defer mockServer.Close() 1671 1672 mockServerURL, _ := url.Parse(mockServer.URL) 1673 cfg := store.DefaultConfig() 1674 cfg.StoreBaseURL = mockServerURL 1675 cfg.Series = "21" 1676 cfg.Architecture = "archXYZ" 1677 cfg.StoreID = "foo" 1678 sto := store.New(cfg, nil) 1679 1680 // the actual test 1681 spec := store.SnapSpec{ 1682 Name: "hello-world", 1683 } 1684 result, err := sto.SnapInfo(s.ctx, spec, nil) 1685 c.Assert(err, IsNil) 1686 c.Check(result.InstanceName(), Equals, "hello-world") 1687 } 1688 1689 func (s *storeTestSuite) TestStoreIDFromAuthContext(c *C) { 1690 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1691 assertRequest(c, r, "GET", infoPathPattern) 1692 storeID := r.Header.Get("Snap-Device-Store") 1693 c.Check(storeID, Equals, "my-brand-store-id") 1694 1695 w.WriteHeader(200) 1696 io.WriteString(w, mockInfoJSON) 1697 })) 1698 1699 c.Assert(mockServer, NotNil) 1700 defer mockServer.Close() 1701 1702 mockServerURL, _ := url.Parse(mockServer.URL) 1703 cfg := store.DefaultConfig() 1704 cfg.StoreBaseURL = mockServerURL 1705 cfg.Series = "21" 1706 cfg.Architecture = "archXYZ" 1707 cfg.StoreID = "fallback" 1708 sto := store.New(cfg, &testDauthContext{c: c, device: s.device, storeID: "my-brand-store-id"}) 1709 1710 // the actual test 1711 spec := store.SnapSpec{ 1712 Name: "hello-world", 1713 } 1714 result, err := sto.SnapInfo(s.ctx, spec, nil) 1715 c.Assert(err, IsNil) 1716 c.Check(result.InstanceName(), Equals, "hello-world") 1717 } 1718 1719 func (s *storeTestSuite) TestProxyStoreFromAuthContext(c *C) { 1720 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1721 assertRequest(c, r, "GET", infoPathPattern) 1722 1723 w.WriteHeader(200) 1724 io.WriteString(w, mockInfoJSON) 1725 })) 1726 1727 c.Assert(mockServer, NotNil) 1728 defer mockServer.Close() 1729 1730 mockServerURL, _ := url.Parse(mockServer.URL) 1731 nowhereURL, err := url.Parse("http://nowhere.invalid") 1732 c.Assert(err, IsNil) 1733 cfg := store.DefaultConfig() 1734 cfg.StoreBaseURL = nowhereURL 1735 sto := store.New(cfg, &testDauthContext{ 1736 c: c, 1737 device: s.device, 1738 proxyStoreID: "foo", 1739 proxyStoreURL: mockServerURL, 1740 }) 1741 1742 // the actual test 1743 spec := store.SnapSpec{ 1744 Name: "hello-world", 1745 } 1746 result, err := sto.SnapInfo(s.ctx, spec, nil) 1747 c.Assert(err, IsNil) 1748 c.Check(result.InstanceName(), Equals, "hello-world") 1749 } 1750 1751 func (s *storeTestSuite) TestProxyStoreFromAuthContextURLFallback(c *C) { 1752 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1753 assertRequest(c, r, "GET", infoPathPattern) 1754 1755 w.WriteHeader(200) 1756 io.WriteString(w, mockInfoJSON) 1757 })) 1758 1759 c.Assert(mockServer, NotNil) 1760 defer mockServer.Close() 1761 1762 mockServerURL, _ := url.Parse(mockServer.URL) 1763 cfg := store.DefaultConfig() 1764 cfg.StoreBaseURL = mockServerURL 1765 sto := store.New(cfg, &testDauthContext{ 1766 c: c, 1767 device: s.device, 1768 // mock an assertion that has id but no url 1769 proxyStoreID: "foo", 1770 proxyStoreURL: nil, 1771 }) 1772 1773 // the actual test 1774 spec := store.SnapSpec{ 1775 Name: "hello-world", 1776 } 1777 result, err := sto.SnapInfo(s.ctx, spec, nil) 1778 c.Assert(err, IsNil) 1779 c.Check(result.InstanceName(), Equals, "hello-world") 1780 } 1781 1782 func (s *storeTestSuite) TestInfoOopses(c *C) { 1783 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1784 assertRequest(c, r, "GET", infoPathPattern) 1785 c.Check(r.URL.Path, Matches, ".*/hello-world") 1786 1787 w.Header().Set("X-Oops-Id", "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f") 1788 w.WriteHeader(500) 1789 1790 io.WriteString(w, `{"oops": "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f"}`) 1791 })) 1792 1793 c.Assert(mockServer, NotNil) 1794 defer mockServer.Close() 1795 1796 mockServerURL, _ := url.Parse(mockServer.URL) 1797 cfg := store.Config{ 1798 StoreBaseURL: mockServerURL, 1799 } 1800 sto := store.New(&cfg, nil) 1801 1802 // the actual test 1803 spec := store.SnapSpec{ 1804 Name: "hello-world", 1805 } 1806 _, err := sto.SnapInfo(s.ctx, spec, nil) 1807 c.Assert(err, ErrorMatches, `cannot get details for snap "hello-world": got unexpected HTTP status code 5.. via GET to "http://\S+" \[OOPS-[[:xdigit:]]*\]`) 1808 } 1809 1810 /* 1811 acquired via 1812 1813 http --pretty=format --print b https://api.snapcraft.io/v2/snaps/info/no:such:package architecture==amd64 fields==architectures,base,confinement,contact,created-at,description,download,epoch,license,name,prices,private,publisher,revision,snap-id,snap-yaml,summary,title,type,version,media,common-ids Snap-Device-Series:16 | xsel -b 1814 1815 on 2018-06-14 1816 1817 */ 1818 const MockNoDetailsJSON = `{ 1819 "error-list": [ 1820 { 1821 "code": "resource-not-found", 1822 "message": "No snap named 'no:such:package' found in series '16'." 1823 } 1824 ] 1825 }` 1826 1827 func (s *storeTestSuite) TestNoInfo(c *C) { 1828 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1829 assertRequest(c, r, "GET", infoPathPattern) 1830 c.Check(r.URL.Path, Matches, ".*/no-such-pkg") 1831 1832 w.WriteHeader(404) 1833 io.WriteString(w, MockNoDetailsJSON) 1834 })) 1835 1836 c.Assert(mockServer, NotNil) 1837 defer mockServer.Close() 1838 1839 mockServerURL, _ := url.Parse(mockServer.URL) 1840 cfg := store.Config{ 1841 StoreBaseURL: mockServerURL, 1842 } 1843 sto := store.New(&cfg, nil) 1844 1845 // the actual test 1846 spec := store.SnapSpec{ 1847 Name: "no-such-pkg", 1848 } 1849 result, err := sto.SnapInfo(s.ctx, spec, nil) 1850 c.Assert(err, NotNil) 1851 c.Assert(result, IsNil) 1852 } 1853 1854 /* acquired via looking at the query snapd does for "snap find 'hello-world of snaps' --narrow" (on core) and adding size=1: 1855 curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://api.snapcraft.io/api/v1/snaps/search?confinement=strict&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Clast_updated%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Csnap_id%2Clicense%2Cbase%2Cmedia%2Csupport_url%2Ccontact%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cdeveloper_name%2Cdeveloper_validation%2Cprivate%2Cconfinement%2Ccommon_ids&q=hello-world+of+snaps&size=1' | python -m json.tool | xsel -b 1856 1857 And then add base and prices, increase title's length, and remove the _links dict 1858 */ 1859 const MockSearchJSON = `{ 1860 "_embedded": { 1861 "clickindex:package": [ 1862 { 1863 "anon_download_url": "https://api.snapcraft.io/api/v1/snaps/download/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap", 1864 "architecture": [ 1865 "all" 1866 ], 1867 "base": "bare-base", 1868 "binary_filesize": 20480, 1869 "channel": "stable", 1870 "common_ids": [], 1871 "confinement": "strict", 1872 "contact": "mailto:snappy-devel@lists.ubuntu.com", 1873 "content": "application", 1874 "description": "This is a simple hello world example.", 1875 "developer_id": "canonical", 1876 "developer_name": "Canonical", 1877 "developer_validation": "verified", 1878 "download_sha3_384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9", 1879 "download_url": "https://api.snapcraft.io/api/v1/snaps/download/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap", 1880 "last_updated": "2016-07-12T16:37:23.960632+00:00", 1881 "license": "MIT", 1882 "media": [ 1883 { 1884 "type": "icon", 1885 "url": "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png" 1886 }, 1887 { 1888 "type": "screenshot", 1889 "url": "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png" 1890 } 1891 ], 1892 "origin": "canonical", 1893 "package_name": "hello-world", 1894 "prices": {"EUR": 2.99, "USD": 3.49}, 1895 "private": false, 1896 "publisher": "Canonical", 1897 "ratings_average": 0.0, 1898 "revision": 27, 1899 "snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1900 "summary": "The 'hello-world' of snaps", 1901 "support_url": "", 1902 "title": "This Is The Most Fantastical Snap of Hello World", 1903 "version": "6.3" 1904 } 1905 ] 1906 } 1907 } 1908 ` 1909 1910 // curl -H 'Snap-Device-Series:16' 'https://api.snapcraft.io/v2/snaps/search?architecture=amd64&confinement=strict%2Cclassic&fields=base%2Cconfinement%2Ccontact%2Cdescription%2Cdownload%2Clicense%2Cprices%2Cprivate%2Cpublisher%2Crevision%2Csummary%2Ctitle%2Ctype%2Cversion%2Cmedia%2Cchannel&q=hello-world+of+snaps' 1911 const MockSearchJSONv2 = ` 1912 { 1913 "results" : [ 1914 { 1915 "name" : "hello-world", 1916 "snap-id" : "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ", 1917 "revision" : { 1918 "base" : "bare-base", 1919 "download" : { 1920 "size" : 20480 1921 }, 1922 "type" : "app", 1923 "version" : "6.3", 1924 "confinement" : "strict", 1925 "revision" : 27, 1926 "common-ids" : ["aaa", "bbb"], 1927 "channel" : "stable" 1928 }, 1929 "snap" : { 1930 "publisher" : { 1931 "username" : "canonical", 1932 "validation" : "verified", 1933 "id" : "canonical", 1934 "display-name" : "Canonical" 1935 }, 1936 "contact" : "mailto:snappy-devel@lists.ubuntu.com", 1937 "media" : [ 1938 { 1939 "type" : "icon", 1940 "url" : "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png" 1941 }, 1942 { 1943 "type" : "screenshot", 1944 "url" : "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png" 1945 } 1946 ], 1947 "summary" : "The 'hello-world' of snaps", 1948 "store-url" : "https://snapcraft.io/hello-world", 1949 "website": "https://ubuntu.com", 1950 "private" : false, 1951 "prices": {"EUR": "2.99", "USD": "3.49"}, 1952 "description" : "This is a simple hello world example.", 1953 "license" : "MIT", 1954 "title" : "This Is The Most Fantastical Snap of Hello World" 1955 } 1956 } 1957 ] 1958 } 1959 ` 1960 1961 const storeVerWithV1Search = "18" 1962 1963 func forceSearchV1(w http.ResponseWriter) { 1964 w.Header().Set("Snap-Store-Version", storeVerWithV1Search) 1965 http.Error(w, http.StatusText(404), 404) 1966 } 1967 1968 func (s *storeTestSuite) TestFindV1Queries(c *C) { 1969 n := 0 1970 var v1Fallback bool 1971 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1972 if strings.Contains(r.URL.Path, findPath) { 1973 forceSearchV1(w) 1974 return 1975 } 1976 v1Fallback = true 1977 assertRequest(c, r, "GET", searchPath) 1978 // check device authorization is set, implicitly checking doRequest was used 1979 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 1980 1981 query := r.URL.Query() 1982 1983 name := query.Get("name") 1984 q := query.Get("q") 1985 section := query.Get("section") 1986 1987 c.Check(r.URL.Path, Matches, ".*/search") 1988 c.Check(query.Get("fields"), Equals, "abc,def") 1989 1990 // write dummy json so that Find doesn't re-try due to json decoder EOF error 1991 io.WriteString(w, "{}") 1992 1993 switch n { 1994 case 0: 1995 c.Check(name, Equals, "hello") 1996 c.Check(q, Equals, "") 1997 c.Check(query.Get("scope"), Equals, "") 1998 c.Check(section, Equals, "") 1999 case 1: 2000 c.Check(name, Equals, "") 2001 c.Check(q, Equals, "hello") 2002 c.Check(query.Get("scope"), Equals, "wide") 2003 c.Check(section, Equals, "") 2004 case 2: 2005 c.Check(name, Equals, "") 2006 c.Check(q, Equals, "") 2007 c.Check(query.Get("scope"), Equals, "") 2008 c.Check(section, Equals, "db") 2009 case 3: 2010 c.Check(name, Equals, "") 2011 c.Check(q, Equals, "hello") 2012 c.Check(query.Get("scope"), Equals, "") 2013 c.Check(section, Equals, "db") 2014 default: 2015 c.Fatalf("what? %d", n) 2016 } 2017 2018 n++ 2019 })) 2020 c.Assert(mockServer, NotNil) 2021 defer mockServer.Close() 2022 2023 mockServerURL, _ := url.Parse(mockServer.URL) 2024 cfg := store.Config{ 2025 StoreBaseURL: mockServerURL, 2026 DetailFields: []string{"abc", "def"}, 2027 } 2028 dauthCtx := &testDauthContext{c: c, device: s.device} 2029 sto := store.New(&cfg, dauthCtx) 2030 2031 for _, query := range []store.Search{ 2032 {Query: "hello", Prefix: true}, 2033 {Query: "hello", Scope: "wide"}, 2034 {Category: "db"}, 2035 {Query: "hello", Category: "db"}, 2036 } { 2037 sto.Find(s.ctx, &query, nil) 2038 } 2039 c.Check(n, Equals, 4) 2040 c.Check(v1Fallback, Equals, true) 2041 } 2042 2043 /* acquired via: 2044 curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://api.snapcraft.io/api/v1/snaps/sections' 2045 */ 2046 const MockSectionsJSON = `{ 2047 "_embedded": { 2048 "clickindex:sections": [ 2049 { 2050 "name": "featured" 2051 }, 2052 { 2053 "name": "database" 2054 } 2055 ] 2056 }, 2057 "_links": { 2058 "self": { 2059 "href": "http://api.snapcraft.io/api/v1/snaps/sections" 2060 } 2061 } 2062 } 2063 ` 2064 2065 func (s *storeTestSuite) TestSectionsQuery(c *C) { 2066 n := 0 2067 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2068 assertRequest(c, r, "GET", sectionsPath) 2069 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 2070 2071 switch n { 2072 case 0: 2073 // All good. 2074 default: 2075 c.Fatalf("what? %d", n) 2076 } 2077 2078 w.Header().Set("Content-Type", "application/hal+json") 2079 w.WriteHeader(200) 2080 io.WriteString(w, MockSectionsJSON) 2081 n++ 2082 })) 2083 c.Assert(mockServer, NotNil) 2084 defer mockServer.Close() 2085 2086 serverURL, _ := url.Parse(mockServer.URL) 2087 cfg := store.Config{ 2088 StoreBaseURL: serverURL, 2089 } 2090 dauthCtx := &testDauthContext{c: c, device: s.device} 2091 sto := store.New(&cfg, dauthCtx) 2092 2093 sections, err := sto.Sections(s.ctx, s.user) 2094 c.Check(err, IsNil) 2095 c.Check(sections, DeepEquals, []string{"featured", "database"}) 2096 c.Check(n, Equals, 1) 2097 } 2098 2099 func (s *storeTestSuite) TestSectionsQueryTooMany(c *C) { 2100 n := 0 2101 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2102 assertRequest(c, r, "GET", sectionsPath) 2103 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 2104 2105 switch n { 2106 case 0: 2107 // All good. 2108 default: 2109 c.Fatalf("what? %d", n) 2110 } 2111 2112 w.WriteHeader(429) 2113 n++ 2114 })) 2115 c.Assert(mockServer, NotNil) 2116 defer mockServer.Close() 2117 2118 serverURL, _ := url.Parse(mockServer.URL) 2119 cfg := store.Config{ 2120 StoreBaseURL: serverURL, 2121 } 2122 dauthCtx := &testDauthContext{c: c, device: s.device} 2123 sto := store.New(&cfg, dauthCtx) 2124 2125 sections, err := sto.Sections(s.ctx, s.user) 2126 c.Check(err, Equals, store.ErrTooManyRequests) 2127 c.Check(sections, IsNil) 2128 c.Check(n, Equals, 1) 2129 } 2130 2131 func (s *storeTestSuite) TestSectionsQueryCustomStore(c *C) { 2132 n := 0 2133 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2134 assertRequest(c, r, "GET", sectionsPath) 2135 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2136 2137 switch n { 2138 case 0: 2139 // All good. 2140 default: 2141 c.Fatalf("what? %d", n) 2142 } 2143 2144 w.Header().Set("Content-Type", "application/hal+json") 2145 w.WriteHeader(200) 2146 io.WriteString(w, MockSectionsJSON) 2147 n++ 2148 })) 2149 c.Assert(mockServer, NotNil) 2150 defer mockServer.Close() 2151 2152 serverURL, _ := url.Parse(mockServer.URL) 2153 cfg := store.Config{ 2154 StoreBaseURL: serverURL, 2155 } 2156 dauthCtx := &testDauthContext{c: c, device: s.device, storeID: "my-brand-store"} 2157 sto := store.New(&cfg, dauthCtx) 2158 2159 sections, err := sto.Sections(s.ctx, s.user) 2160 c.Check(err, IsNil) 2161 c.Check(sections, DeepEquals, []string{"featured", "database"}) 2162 } 2163 2164 const mockNamesJSON = ` 2165 { 2166 "_embedded": { 2167 "clickindex:package": [ 2168 { 2169 "aliases": [ 2170 { 2171 "name": "potato", 2172 "target": "baz" 2173 }, 2174 { 2175 "name": "meh", 2176 "target": "baz" 2177 } 2178 ], 2179 "apps": ["baz"], 2180 "title": "a title", 2181 "summary": "oneary plus twoary", 2182 "package_name": "bar", 2183 "version": "2.0" 2184 }, 2185 { 2186 "aliases": [{"name": "meh", "target": "foo"}], 2187 "apps": ["foo"], 2188 "package_name": "foo", 2189 "version": "1.0" 2190 } 2191 ] 2192 } 2193 }` 2194 2195 func (s *storeTestSuite) TestSnapCommandsOnClassic(c *C) { 2196 s.testSnapCommands(c, true) 2197 } 2198 2199 func (s *storeTestSuite) TestSnapCommandsOnCore(c *C) { 2200 s.testSnapCommands(c, false) 2201 } 2202 2203 func (s *storeTestSuite) testSnapCommands(c *C, onClassic bool) { 2204 c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) 2205 defer release.MockOnClassic(onClassic)() 2206 2207 n := 0 2208 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2209 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 2210 2211 switch n { 2212 case 0: 2213 query := r.URL.Query() 2214 c.Check(query, HasLen, 1) 2215 expectedConfinement := "strict" 2216 if onClassic { 2217 expectedConfinement = "strict,classic" 2218 } 2219 c.Check(query.Get("confinement"), Equals, expectedConfinement) 2220 c.Check(r.URL.Path, Equals, "/api/v1/snaps/names") 2221 default: 2222 c.Fatalf("what? %d", n) 2223 } 2224 2225 w.Header().Set("Content-Type", "application/hal+json") 2226 w.Header().Set("Content-Length", fmt.Sprint(len(mockNamesJSON))) 2227 w.WriteHeader(200) 2228 io.WriteString(w, mockNamesJSON) 2229 n++ 2230 })) 2231 c.Assert(mockServer, NotNil) 2232 defer mockServer.Close() 2233 2234 serverURL, _ := url.Parse(mockServer.URL) 2235 dauthCtx := &testDauthContext{c: c, device: s.device} 2236 sto := store.New(&store.Config{StoreBaseURL: serverURL}, dauthCtx) 2237 2238 db, err := advisor.Create() 2239 c.Assert(err, IsNil) 2240 defer db.Rollback() 2241 2242 var bufNames bytes.Buffer 2243 err = sto.WriteCatalogs(s.ctx, &bufNames, db) 2244 c.Assert(err, IsNil) 2245 db.Commit() 2246 c.Check(bufNames.String(), Equals, "bar\nfoo\n") 2247 2248 dump, err := advisor.DumpCommands() 2249 c.Assert(err, IsNil) 2250 c.Check(dump, DeepEquals, map[string]string{ 2251 "foo": `[{"snap":"foo","version":"1.0"}]`, 2252 "bar.baz": `[{"snap":"bar","version":"2.0"}]`, 2253 "potato": `[{"snap":"bar","version":"2.0"}]`, 2254 "meh": `[{"snap":"bar","version":"2.0"},{"snap":"foo","version":"1.0"}]`, 2255 }) 2256 c.Check(n, Equals, 1) 2257 } 2258 2259 func (s *storeTestSuite) TestSnapCommandsTooMany(c *C) { 2260 c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) 2261 2262 n := 0 2263 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2264 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 2265 2266 switch n { 2267 case 0: 2268 c.Check(r.URL.Path, Equals, "/api/v1/snaps/names") 2269 default: 2270 c.Fatalf("what? %d", n) 2271 } 2272 2273 w.WriteHeader(429) 2274 n++ 2275 })) 2276 c.Assert(mockServer, NotNil) 2277 defer mockServer.Close() 2278 2279 serverURL, _ := url.Parse(mockServer.URL) 2280 dauthCtx := &testDauthContext{c: c, device: s.device} 2281 sto := store.New(&store.Config{StoreBaseURL: serverURL}, dauthCtx) 2282 2283 db, err := advisor.Create() 2284 c.Assert(err, IsNil) 2285 defer db.Rollback() 2286 2287 var bufNames bytes.Buffer 2288 err = sto.WriteCatalogs(s.ctx, &bufNames, db) 2289 c.Assert(err, Equals, store.ErrTooManyRequests) 2290 db.Commit() 2291 c.Check(bufNames.String(), Equals, "") 2292 2293 dump, err := advisor.DumpCommands() 2294 c.Assert(err, IsNil) 2295 c.Check(dump, HasLen, 0) 2296 c.Check(n, Equals, 1) 2297 } 2298 2299 func (s *storeTestSuite) testFind(c *C, apiV1 bool) { 2300 restore := release.MockOnClassic(false) 2301 defer restore() 2302 2303 var v1Fallback, v2Hit bool 2304 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2305 if apiV1 { 2306 if strings.Contains(r.URL.Path, findPath) { 2307 forceSearchV1(w) 2308 return 2309 } 2310 v1Fallback = true 2311 assertRequest(c, r, "GET", searchPath) 2312 } else { 2313 v2Hit = true 2314 assertRequest(c, r, "GET", findPath) 2315 } 2316 query := r.URL.Query() 2317 2318 q := query.Get("q") 2319 c.Check(q, Equals, "hello") 2320 2321 c.Check(r.UserAgent(), Equals, userAgent) 2322 2323 if apiV1 { 2324 // check device authorization is set, implicitly checking doRequest was used 2325 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2326 2327 // no store ID by default 2328 storeID := r.Header.Get("X-Ubuntu-Store") 2329 c.Check(storeID, Equals, "") 2330 2331 c.Check(r.URL.Query().Get("fields"), Equals, "abc,def") 2332 2333 c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, release.Series) 2334 c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, arch.DpkgArchitecture()) 2335 c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "false") 2336 2337 c.Check(r.Header.Get("X-Ubuntu-Confinement"), Equals, "") 2338 2339 w.Header().Set("X-Suggested-Currency", "GBP") 2340 2341 w.Header().Set("Content-Type", "application/hal+json") 2342 w.WriteHeader(200) 2343 2344 io.WriteString(w, MockSearchJSON) 2345 } else { 2346 2347 // check device authorization is set, implicitly checking doRequest was used 2348 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2349 2350 // no store ID by default 2351 storeID := r.Header.Get("Snap-Device-Store") 2352 c.Check(storeID, Equals, "") 2353 2354 c.Check(r.URL.Query().Get("fields"), Equals, "abc,def") 2355 2356 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 2357 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 2358 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 2359 2360 w.Header().Set("X-Suggested-Currency", "GBP") 2361 2362 w.Header().Set("Content-Type", "application/json") 2363 w.WriteHeader(200) 2364 2365 io.WriteString(w, MockSearchJSONv2) 2366 } 2367 })) 2368 2369 c.Assert(mockServer, NotNil) 2370 defer mockServer.Close() 2371 2372 mockServerURL, _ := url.Parse(mockServer.URL) 2373 cfg := store.Config{ 2374 StoreBaseURL: mockServerURL, 2375 DetailFields: []string{"abc", "def"}, 2376 FindFields: []string{"abc", "def"}, 2377 } 2378 2379 dauthCtx := &testDauthContext{c: c, device: s.device} 2380 sto := store.New(&cfg, dauthCtx) 2381 2382 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2383 c.Assert(err, IsNil) 2384 c.Assert(snaps, HasLen, 1) 2385 snp := snaps[0] 2386 c.Check(snp.InstanceName(), Equals, "hello-world") 2387 c.Check(snp.Revision, Equals, snap.R(27)) 2388 c.Check(snp.SnapID, Equals, helloWorldSnapID) 2389 c.Check(snp.Publisher, Equals, snap.StoreAccount{ 2390 ID: "canonical", 2391 Username: "canonical", 2392 DisplayName: "Canonical", 2393 Validation: "verified", 2394 }) 2395 c.Check(snp.Version, Equals, "6.3") 2396 c.Check(snp.Size, Equals, int64(20480)) 2397 c.Check(snp.Channel, Equals, "stable") 2398 c.Check(snp.Description(), Equals, "This is a simple hello world example.") 2399 c.Check(snp.Summary(), Equals, "The 'hello-world' of snaps") 2400 c.Check(snp.Title(), Equals, "This Is The Most Fantastical Snap of He…") 2401 c.Check(snp.License, Equals, "MIT") 2402 // this is more a "we know this isn't there" than an actual test for a wanted feature 2403 // NOTE snap.Epoch{} (which prints as "0", and is thus Unset) is not a valid Epoch. 2404 c.Check(snp.Epoch, DeepEquals, snap.Epoch{}) 2405 c.Assert(snp.Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49}) 2406 c.Assert(snp.Paid, Equals, true) 2407 c.Assert(snp.Media, DeepEquals, snap.MediaInfos{ 2408 { 2409 Type: "icon", 2410 URL: "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", 2411 }, { 2412 Type: "screenshot", 2413 URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png", 2414 }, 2415 }) 2416 c.Check(snp.MustBuy, Equals, true) 2417 c.Check(snp.Contact, Equals, "mailto:snappy-devel@lists.ubuntu.com") 2418 c.Check(snp.Base, Equals, "bare-base") 2419 2420 // Make sure the epoch (currently not sent by the store) defaults to "0" 2421 c.Check(snp.Epoch.String(), Equals, "0") 2422 2423 c.Check(sto.SuggestedCurrency(), Equals, "GBP") 2424 2425 if apiV1 { 2426 c.Check(snp.Architectures, DeepEquals, []string{"all"}) 2427 c.Check(snp.Sha3_384, Matches, `[[:xdigit:]]{96}`) 2428 c.Check(v1Fallback, Equals, true) 2429 } else { 2430 c.Check(snp.Website, Equals, "https://ubuntu.com") 2431 c.Check(snp.StoreURL, Equals, "https://snapcraft.io/hello-world") 2432 c.Check(snp.CommonIDs, DeepEquals, []string{"aaa", "bbb"}) 2433 c.Check(v2Hit, Equals, true) 2434 } 2435 } 2436 2437 func (s *storeTestSuite) TestFindV1(c *C) { 2438 apiV1 := true 2439 s.testFind(c, apiV1) 2440 } 2441 2442 func (s *storeTestSuite) TestFindV2(c *C) { 2443 s.testFind(c, false) 2444 } 2445 2446 func (s *storeTestSuite) TestFindV2FindFields(c *C) { 2447 dauthCtx := &testDauthContext{c: c, device: s.device} 2448 sto := store.New(nil, dauthCtx) 2449 2450 findFields := sto.FindFields() 2451 sort.Strings(findFields) 2452 c.Assert(findFields, DeepEquals, []string{ 2453 "base", "channel", "common-ids", "confinement", "contact", 2454 "description", "download", "license", "media", "prices", "private", 2455 "publisher", "revision", "store-url", "summary", "title", "type", 2456 "version", "website"}) 2457 } 2458 2459 func (s *storeTestSuite) testFindPrivate(c *C, apiV1 bool) { 2460 n := 0 2461 var v1Fallback, v2Hit bool 2462 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2463 if apiV1 { 2464 if strings.Contains(r.URL.Path, findPath) { 2465 forceSearchV1(w) 2466 return 2467 } 2468 v1Fallback = true 2469 assertRequest(c, r, "GET", searchPath) 2470 } else { 2471 v2Hit = true 2472 assertRequest(c, r, "GET", findPath) 2473 } 2474 2475 query := r.URL.Query() 2476 name := query.Get("name") 2477 q := query.Get("q") 2478 2479 switch n { 2480 case 0: 2481 if apiV1 { 2482 c.Check(r.URL.Path, Matches, ".*/search") 2483 } else { 2484 c.Check(r.URL.Path, Matches, ".*/find") 2485 } 2486 c.Check(name, Equals, "") 2487 c.Check(q, Equals, "foo") 2488 c.Check(query.Get("private"), Equals, "true") 2489 case 1: 2490 if apiV1 { 2491 c.Check(r.URL.Path, Matches, ".*/search") 2492 } else { 2493 c.Check(r.URL.Path, Matches, ".*/find") 2494 } 2495 c.Check(name, Equals, "foo") 2496 c.Check(q, Equals, "") 2497 c.Check(query.Get("private"), Equals, "true") 2498 default: 2499 c.Fatalf("what? %d", n) 2500 } 2501 2502 if apiV1 { 2503 w.Header().Set("Content-Type", "application/hal+json") 2504 w.WriteHeader(200) 2505 io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1)) 2506 2507 } else { 2508 w.Header().Set("Content-Type", "application/json") 2509 w.WriteHeader(200) 2510 io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": "2.99", "USD": "3.49"`, "", -1)) 2511 } 2512 2513 n++ 2514 })) 2515 c.Assert(mockServer, NotNil) 2516 defer mockServer.Close() 2517 2518 serverURL, _ := url.Parse(mockServer.URL) 2519 cfg := store.Config{ 2520 StoreBaseURL: serverURL, 2521 } 2522 2523 sto := store.New(&cfg, nil) 2524 2525 _, err := sto.Find(s.ctx, &store.Search{Query: "foo", Private: true}, s.user) 2526 c.Check(err, IsNil) 2527 2528 _, err = sto.Find(s.ctx, &store.Search{Query: "foo", Prefix: true, Private: true}, s.user) 2529 c.Check(err, IsNil) 2530 2531 _, err = sto.Find(s.ctx, &store.Search{Query: "foo", Private: true}, nil) 2532 c.Check(err, Equals, store.ErrUnauthenticated) 2533 2534 _, err = sto.Find(s.ctx, &store.Search{Query: "name:foo", Private: true}, s.user) 2535 c.Check(err, Equals, store.ErrBadQuery) 2536 2537 c.Check(n, Equals, 2) 2538 2539 if apiV1 { 2540 c.Check(v1Fallback, Equals, true) 2541 } else { 2542 c.Check(v2Hit, Equals, true) 2543 } 2544 } 2545 2546 func (s *storeTestSuite) TestFindV1Private(c *C) { 2547 apiV1 := true 2548 s.testFindPrivate(c, apiV1) 2549 } 2550 2551 func (s *storeTestSuite) TestFindV2Private(c *C) { 2552 s.testFindPrivate(c, false) 2553 } 2554 2555 func (s *storeTestSuite) TestFindV2ErrorList(c *C) { 2556 const errJSON = `{ 2557 "error-list": [ 2558 { 2559 "code": "api-error", 2560 "message": "api error occurred" 2561 } 2562 ] 2563 }` 2564 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2565 assertRequest(c, r, "GET", findPath) 2566 w.Header().Set("Content-Type", "application/json") 2567 w.WriteHeader(400) 2568 io.WriteString(w, errJSON) 2569 })) 2570 c.Assert(mockServer, NotNil) 2571 defer mockServer.Close() 2572 2573 mockServerURL, _ := url.Parse(mockServer.URL) 2574 cfg := store.Config{ 2575 StoreBaseURL: mockServerURL, 2576 FindFields: []string{}, 2577 } 2578 sto := store.New(&cfg, nil) 2579 _, err := sto.Find(s.ctx, &store.Search{Query: "x"}, nil) 2580 c.Check(err, ErrorMatches, `api error occurred`) 2581 } 2582 2583 func (s *storeTestSuite) TestFindFailures(c *C) { 2584 // bad query check is done early in Find(), so the test covers both search 2585 // v1 & v2 2586 sto := store.New(&store.Config{StoreBaseURL: new(url.URL)}, nil) 2587 _, err := sto.Find(s.ctx, &store.Search{Query: "foo:bar"}, nil) 2588 c.Check(err, Equals, store.ErrBadQuery) 2589 } 2590 2591 func (s *storeTestSuite) TestFindInvalidScope(c *C) { 2592 // bad query check is done early in Find(), so the test covers both search 2593 // v1 & v2 2594 sto := store.New(&store.Config{StoreBaseURL: new(url.URL)}, nil) 2595 _, err := sto.Find(s.ctx, &store.Search{Query: "", Scope: "foo"}, nil) 2596 c.Check(err, Equals, store.ErrInvalidScope) 2597 } 2598 2599 func (s *storeTestSuite) testFindFails(c *C, apiV1 bool) { 2600 var v1Fallback, v2Hit bool 2601 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2602 if apiV1 { 2603 if strings.Contains(r.URL.Path, findPath) { 2604 forceSearchV1(w) 2605 return 2606 } 2607 v1Fallback = true 2608 assertRequest(c, r, "GET", searchPath) 2609 } else { 2610 assertRequest(c, r, "GET", findPath) 2611 v2Hit = true 2612 } 2613 c.Check(r.URL.Query().Get("q"), Equals, "hello") 2614 http.Error(w, http.StatusText(418), 418) // I'm a teapot 2615 })) 2616 c.Assert(mockServer, NotNil) 2617 defer mockServer.Close() 2618 2619 mockServerURL, _ := url.Parse(mockServer.URL) 2620 cfg := store.Config{ 2621 StoreBaseURL: mockServerURL, 2622 DetailFields: []string{}, // make the error less noisy 2623 FindFields: []string{}, 2624 } 2625 sto := store.New(&cfg, nil) 2626 2627 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2628 c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 418 via GET to "http://\S+[?&]q=hello.*"`) 2629 c.Check(snaps, HasLen, 0) 2630 if apiV1 { 2631 c.Check(v1Fallback, Equals, true) 2632 } else { 2633 c.Check(v2Hit, Equals, true) 2634 } 2635 } 2636 2637 func (s *storeTestSuite) TestFindV1Fails(c *C) { 2638 apiV1 := true 2639 s.testFindFails(c, apiV1) 2640 } 2641 2642 func (s *storeTestSuite) TestFindV2Fails(c *C) { 2643 s.testFindFails(c, false) 2644 } 2645 2646 func (s *storeTestSuite) testFindBadContentType(c *C, apiV1 bool) { 2647 var v1Fallback, v2Hit bool 2648 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2649 if apiV1 { 2650 if strings.Contains(r.URL.Path, findPath) { 2651 forceSearchV1(w) 2652 return 2653 } 2654 v1Fallback = true 2655 assertRequest(c, r, "GET", searchPath) 2656 } else { 2657 v2Hit = true 2658 assertRequest(c, r, "GET", findPath) 2659 } 2660 c.Check(r.URL.Query().Get("q"), Equals, "hello") 2661 if apiV1 { 2662 io.WriteString(w, MockSearchJSON) 2663 } else { 2664 io.WriteString(w, MockSearchJSONv2) 2665 } 2666 })) 2667 c.Assert(mockServer, NotNil) 2668 defer mockServer.Close() 2669 2670 mockServerURL, _ := url.Parse(mockServer.URL) 2671 cfg := store.Config{ 2672 StoreBaseURL: mockServerURL, 2673 DetailFields: []string{}, // make the error less noisy 2674 FindFields: []string{}, 2675 } 2676 sto := store.New(&cfg, nil) 2677 2678 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2679 c.Check(err, ErrorMatches, `received an unexpected content type \("text/plain[^"]+"\) when trying to search via "http://\S+[?&]q=hello.*"`) 2680 c.Check(snaps, HasLen, 0) 2681 if apiV1 { 2682 c.Check(v1Fallback, Equals, true) 2683 } else { 2684 c.Check(v2Hit, Equals, true) 2685 } 2686 } 2687 2688 func (s *storeTestSuite) TestFindV1BadContentType(c *C) { 2689 apiV1 := true 2690 s.testFindBadContentType(c, apiV1) 2691 } 2692 2693 func (s *storeTestSuite) TestFindV2BadContentType(c *C) { 2694 s.testFindBadContentType(c, false) 2695 } 2696 2697 func (s *storeTestSuite) testFindBadBody(c *C, apiV1 bool) { 2698 var v1Fallback, v2Hit bool 2699 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2700 if apiV1 { 2701 if strings.Contains(r.URL.Path, findPath) { 2702 forceSearchV1(w) 2703 return 2704 } 2705 v1Fallback = true 2706 assertRequest(c, r, "GET", searchPath) 2707 } else { 2708 v2Hit = true 2709 assertRequest(c, r, "GET", findPath) 2710 } 2711 query := r.URL.Query() 2712 c.Check(query.Get("q"), Equals, "hello") 2713 if apiV1 { 2714 w.Header().Set("Content-Type", "application/hal+json") 2715 } else { 2716 w.Header().Set("Content-Type", "application/json") 2717 } 2718 io.WriteString(w, "<hello>") 2719 })) 2720 c.Assert(mockServer, NotNil) 2721 defer mockServer.Close() 2722 2723 mockServerURL, _ := url.Parse(mockServer.URL) 2724 cfg := store.Config{ 2725 StoreBaseURL: mockServerURL, 2726 DetailFields: []string{}, // make the error less noisy 2727 FindFields: []string{}, 2728 } 2729 sto := store.New(&cfg, nil) 2730 2731 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2732 c.Check(err, ErrorMatches, `invalid character '<' looking for beginning of value`) 2733 c.Check(snaps, HasLen, 0) 2734 if apiV1 { 2735 c.Check(v1Fallback, Equals, true) 2736 } else { 2737 c.Check(v2Hit, Equals, true) 2738 } 2739 } 2740 2741 func (s *storeTestSuite) TestFindV1BadBody(c *C) { 2742 apiV1 := true 2743 s.testFindBadBody(c, apiV1) 2744 } 2745 2746 func (s *storeTestSuite) TestFindV2BadBody(c *C) { 2747 s.testFindBadBody(c, false) 2748 } 2749 2750 func (s *storeTestSuite) TestFindV2_404NoFallbackIfNewStore(c *C) { 2751 n := 0 2752 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2753 c.Assert(n, Equals, 0) 2754 n++ 2755 assertRequest(c, r, "GET", findPath) 2756 c.Check(r.URL.Query().Get("q"), Equals, "hello") 2757 w.Header().Set("Snap-Store-Version", "30") 2758 w.WriteHeader(404) 2759 })) 2760 c.Assert(mockServer, NotNil) 2761 defer mockServer.Close() 2762 2763 mockServerURL, _ := url.Parse(mockServer.URL) 2764 cfg := store.Config{ 2765 StoreBaseURL: mockServerURL, 2766 FindFields: []string{}, 2767 } 2768 sto := store.New(&cfg, nil) 2769 2770 _, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2771 c.Check(err, ErrorMatches, `.*got unexpected HTTP status code 404.*`) 2772 c.Check(n, Equals, 1) 2773 } 2774 2775 // testFindPermanent500 checks that a permanent 500 error on every request 2776 // results in 5 retries, after which the caller gets the 500 status. 2777 func (s *storeTestSuite) testFindPermanent500(c *C, apiV1 bool) { 2778 var n = 0 2779 var v1Fallback, v2Hit bool 2780 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2781 if apiV1 { 2782 if strings.Contains(r.URL.Path, findPath) { 2783 forceSearchV1(w) 2784 return 2785 } 2786 v1Fallback = true 2787 assertRequest(c, r, "GET", searchPath) 2788 } else { 2789 v2Hit = true 2790 assertRequest(c, r, "GET", findPath) 2791 } 2792 n++ 2793 w.WriteHeader(500) 2794 })) 2795 c.Assert(mockServer, NotNil) 2796 defer mockServer.Close() 2797 2798 mockServerURL, _ := url.Parse(mockServer.URL) 2799 cfg := store.Config{ 2800 StoreBaseURL: mockServerURL, 2801 DetailFields: []string{}, 2802 FindFields: []string{}, 2803 } 2804 sto := store.New(&cfg, nil) 2805 2806 _, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2807 c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 500 via GET to "http://\S+[?&]q=hello.*"`) 2808 c.Assert(n, Equals, 5) 2809 if apiV1 { 2810 c.Check(v1Fallback, Equals, true) 2811 } else { 2812 c.Check(v2Hit, Equals, true) 2813 } 2814 } 2815 2816 func (s *storeTestSuite) TestFindV1Permanent500(c *C) { 2817 apiV1 := true 2818 s.testFindPermanent500(c, apiV1) 2819 } 2820 2821 func (s *storeTestSuite) TestFindV2Permanent500(c *C) { 2822 s.testFindPermanent500(c, false) 2823 } 2824 2825 // testFind500OnceThenSucceed checks that a single 500 failure, followed by 2826 // a successful response is handled. 2827 func (s *storeTestSuite) testFind500OnceThenSucceed(c *C, apiV1 bool) { 2828 var n = 0 2829 var v1Fallback, v2Hit bool 2830 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2831 if apiV1 { 2832 if strings.Contains(r.URL.Path, findPath) { 2833 forceSearchV1(w) 2834 return 2835 } 2836 v1Fallback = true 2837 assertRequest(c, r, "GET", searchPath) 2838 } else { 2839 v2Hit = true 2840 assertRequest(c, r, "GET", findPath) 2841 } 2842 n++ 2843 if n == 1 { 2844 w.WriteHeader(500) 2845 } else { 2846 if apiV1 { 2847 w.Header().Set("Content-Type", "application/hal+json") 2848 w.WriteHeader(200) 2849 io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1)) 2850 } else { 2851 w.Header().Set("Content-Type", "application/json") 2852 w.WriteHeader(200) 2853 io.WriteString(w, strings.Replace(MockSearchJSONv2, `"EUR": "2.99", "USD": "3.49"`, "", -1)) 2854 } 2855 } 2856 })) 2857 c.Assert(mockServer, NotNil) 2858 defer mockServer.Close() 2859 2860 mockServerURL, _ := url.Parse(mockServer.URL) 2861 cfg := store.Config{ 2862 StoreBaseURL: mockServerURL, 2863 DetailFields: []string{}, 2864 FindFields: []string{}, 2865 } 2866 sto := store.New(&cfg, nil) 2867 2868 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2869 c.Check(err, IsNil) 2870 c.Assert(snaps, HasLen, 1) 2871 c.Assert(n, Equals, 2) 2872 if apiV1 { 2873 c.Check(v1Fallback, Equals, true) 2874 } else { 2875 c.Check(v2Hit, Equals, true) 2876 } 2877 } 2878 2879 func (s *storeTestSuite) TestFindV1_500Once(c *C) { 2880 apiV1 := true 2881 s.testFind500OnceThenSucceed(c, apiV1) 2882 } 2883 2884 func (s *storeTestSuite) TestFindV2_500Once(c *C) { 2885 s.testFind500OnceThenSucceed(c, false) 2886 } 2887 2888 func (s *storeTestSuite) testFindAuthFailed(c *C, apiV1 bool) { 2889 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2890 if apiV1 { 2891 if strings.Contains(r.URL.Path, findPath) { 2892 forceSearchV1(w) 2893 return 2894 } 2895 } 2896 switch r.URL.Path { 2897 case searchPath: 2898 c.Assert(apiV1, Equals, true) 2899 fallthrough 2900 case findPath: 2901 // check authorization is set 2902 authorization := r.Header.Get("Authorization") 2903 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 2904 2905 query := r.URL.Query() 2906 c.Check(query.Get("q"), Equals, "foo") 2907 if release.OnClassic { 2908 c.Check(query.Get("confinement"), Matches, `strict,classic|classic,strict`) 2909 } else { 2910 c.Check(query.Get("confinement"), Equals, "strict") 2911 } 2912 if apiV1 { 2913 w.Header().Set("Content-Type", "application/hal+json") 2914 io.WriteString(w, MockSearchJSON) 2915 } else { 2916 w.Header().Set("Content-Type", "application/json") 2917 io.WriteString(w, MockSearchJSONv2) 2918 } 2919 case ordersPath: 2920 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 2921 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 2922 c.Check(r.URL.Path, Equals, ordersPath) 2923 w.WriteHeader(401) 2924 io.WriteString(w, "{}") 2925 default: 2926 c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path) 2927 } 2928 })) 2929 c.Assert(mockServer, NotNil) 2930 defer mockServer.Close() 2931 2932 mockServerURL, _ := url.Parse(mockServer.URL) 2933 cfg := store.Config{ 2934 StoreBaseURL: mockServerURL, 2935 DetailFields: []string{}, // make the error less noisy 2936 } 2937 sto := store.New(&cfg, nil) 2938 2939 snaps, err := sto.Find(s.ctx, &store.Search{Query: "foo"}, s.user) 2940 c.Assert(err, IsNil) 2941 2942 // Check that we log an error. 2943 c.Check(s.logbuf.String(), Matches, "(?ms).* cannot get user orders: invalid credentials") 2944 2945 // But still successfully return snap information. 2946 c.Assert(snaps, HasLen, 1) 2947 c.Check(snaps[0].SnapID, Equals, helloWorldSnapID) 2948 c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49}) 2949 c.Check(snaps[0].MustBuy, Equals, true) 2950 } 2951 2952 func (s *storeTestSuite) TestFindV1AuthFailed(c *C) { 2953 apiV1 := true 2954 s.testFindAuthFailed(c, apiV1) 2955 } 2956 2957 func (s *storeTestSuite) TestFindV2AuthFailed(c *C) { 2958 s.testFindAuthFailed(c, false) 2959 } 2960 2961 func (s *storeTestSuite) testFindCommonIDs(c *C, apiV1 bool) { 2962 n := 0 2963 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2964 if apiV1 { 2965 if strings.Contains(r.URL.Path, findPath) { 2966 forceSearchV1(w) 2967 return 2968 } 2969 assertRequest(c, r, "GET", searchPath) 2970 } else { 2971 assertRequest(c, r, "GET", findPath) 2972 } 2973 query := r.URL.Query() 2974 2975 name := query.Get("name") 2976 q := query.Get("q") 2977 2978 switch n { 2979 case 0: 2980 if apiV1 { 2981 c.Check(r.URL.Path, Matches, ".*/search") 2982 } else { 2983 c.Check(r.URL.Path, Matches, ".*/find") 2984 } 2985 c.Check(name, Equals, "") 2986 c.Check(q, Equals, "foo") 2987 default: 2988 c.Fatalf("what? %d", n) 2989 } 2990 2991 if apiV1 { 2992 w.Header().Set("Content-Type", "application/hal+json") 2993 w.WriteHeader(200) 2994 io.WriteString(w, strings.Replace(MockSearchJSON, 2995 `"common_ids": []`, 2996 `"common_ids": ["org.hello"]`, -1)) 2997 } else { 2998 w.Header().Set("Content-Type", "application/json") 2999 w.WriteHeader(200) 3000 io.WriteString(w, MockSearchJSONv2) 3001 } 3002 3003 n++ 3004 })) 3005 c.Assert(mockServer, NotNil) 3006 defer mockServer.Close() 3007 3008 serverURL, _ := url.Parse(mockServer.URL) 3009 cfg := store.Config{ 3010 StoreBaseURL: serverURL, 3011 } 3012 sto := store.New(&cfg, nil) 3013 3014 infos, err := sto.Find(s.ctx, &store.Search{Query: "foo"}, nil) 3015 c.Check(err, IsNil) 3016 c.Assert(infos, HasLen, 1) 3017 if apiV1 { 3018 c.Check(infos[0].CommonIDs, DeepEquals, []string{"org.hello"}) 3019 } else { 3020 c.Check(infos[0].CommonIDs, DeepEquals, []string{"aaa", "bbb"}) 3021 } 3022 } 3023 3024 func (s *storeTestSuite) TestFindV1CommonIDs(c *C) { 3025 apiV1 := true 3026 s.testFindCommonIDs(c, apiV1) 3027 } 3028 3029 func (s *storeTestSuite) TestFindV2CommonIDs(c *C) { 3030 s.testFindCommonIDs(c, false) 3031 } 3032 3033 func (s *storeTestSuite) testFindByCommonID(c *C, apiV1 bool) { 3034 n := 0 3035 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3036 if apiV1 { 3037 if strings.Contains(r.URL.Path, findPath) { 3038 forceSearchV1(w) 3039 return 3040 } 3041 assertRequest(c, r, "GET", searchPath) 3042 } else { 3043 assertRequest(c, r, "GET", findPath) 3044 } 3045 query := r.URL.Query() 3046 3047 switch n { 3048 case 0: 3049 if apiV1 { 3050 c.Check(r.URL.Path, Matches, ".*/search") 3051 c.Check(query["common_id"], DeepEquals, []string{"org.hello"}) 3052 } else { 3053 c.Check(r.URL.Path, Matches, ".*/find") 3054 c.Check(query["common-id"], DeepEquals, []string{"org.hello"}) 3055 } 3056 c.Check(query["name"], IsNil) 3057 c.Check(query["q"], IsNil) 3058 default: 3059 c.Fatalf("expected 1 query, now on %d", n+1) 3060 } 3061 3062 if apiV1 { 3063 w.Header().Set("Content-Type", "application/hal+json") 3064 w.WriteHeader(200) 3065 io.WriteString(w, strings.Replace(MockSearchJSON, 3066 `"common_ids": []`, 3067 `"common_ids": ["org.hello"]`, -1)) 3068 } else { 3069 w.Header().Set("Content-Type", "application/json") 3070 w.WriteHeader(200) 3071 io.WriteString(w, MockSearchJSONv2) 3072 } 3073 3074 n++ 3075 })) 3076 c.Assert(mockServer, NotNil) 3077 defer mockServer.Close() 3078 3079 serverURL, _ := url.Parse(mockServer.URL) 3080 cfg := store.Config{ 3081 StoreBaseURL: serverURL, 3082 } 3083 sto := store.New(&cfg, nil) 3084 3085 infos, err := sto.Find(s.ctx, &store.Search{CommonID: "org.hello"}, nil) 3086 c.Check(err, IsNil) 3087 c.Assert(infos, HasLen, 1) 3088 if apiV1 { 3089 c.Check(infos[0].CommonIDs, DeepEquals, []string{"org.hello"}) 3090 } else { 3091 c.Check(infos[0].CommonIDs, DeepEquals, []string{"aaa", "bbb"}) 3092 } 3093 } 3094 3095 func (s *storeTestSuite) TestFindV1ByCommonID(c *C) { 3096 apiV1 := true 3097 s.testFindByCommonID(c, apiV1) 3098 } 3099 3100 func (s *storeTestSuite) TestFindV2ByCommonID(c *C) { 3101 s.testFindByCommonID(c, false) 3102 } 3103 3104 func (s *storeTestSuite) TestFindClientUserAgent(c *C) { 3105 clientUserAgent := "some-client/1.0" 3106 3107 serverWasHit := false 3108 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3109 c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, clientUserAgent) 3110 serverWasHit = true 3111 3112 http.Error(w, http.StatusText(418), 418) // I'm a teapot 3113 })) 3114 c.Assert(mockServer, NotNil) 3115 defer mockServer.Close() 3116 3117 mockServerURL, _ := url.Parse(mockServer.URL) 3118 cfg := store.Config{ 3119 StoreBaseURL: mockServerURL, 3120 DetailFields: []string{}, // make the error less noisy 3121 } 3122 3123 req, err := http.NewRequest("GET", "/", nil) 3124 c.Assert(err, IsNil) 3125 req.Header.Add("User-Agent", clientUserAgent) 3126 ctx := store.WithClientUserAgent(s.ctx, req) 3127 3128 sto := store.New(&cfg, nil) 3129 sto.Find(ctx, &store.Search{Query: "hello"}, nil) 3130 c.Assert(serverWasHit, Equals, true) 3131 } 3132 3133 func (s *storeTestSuite) TestAuthLocationDependsOnEnviron(c *C) { 3134 defer snapdenv.MockUseStagingStore(false)() 3135 before := store.AuthLocation() 3136 3137 snapdenv.MockUseStagingStore(true) 3138 after := store.AuthLocation() 3139 3140 c.Check(before, Not(Equals), after) 3141 } 3142 3143 func (s *storeTestSuite) TestAuthURLDependsOnEnviron(c *C) { 3144 defer snapdenv.MockUseStagingStore(false)() 3145 before := store.AuthURL() 3146 3147 snapdenv.MockUseStagingStore(true) 3148 after := store.AuthURL() 3149 3150 c.Check(before, Not(Equals), after) 3151 } 3152 3153 func (s *storeTestSuite) TestApiURLDependsOnEnviron(c *C) { 3154 defer snapdenv.MockUseStagingStore(false)() 3155 before := store.ApiURL() 3156 3157 snapdenv.MockUseStagingStore(true) 3158 after := store.ApiURL() 3159 3160 c.Check(before, Not(Equals), after) 3161 } 3162 3163 func (s *storeTestSuite) TestStoreURLDependsOnEnviron(c *C) { 3164 // This also depends on the API URL, but that's tested separately (see 3165 // TestApiURLDependsOnEnviron). 3166 api := store.ApiURL() 3167 3168 c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", ""), IsNil) 3169 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", ""), IsNil) 3170 3171 // Test in order of precedence (low first) leaving env vars set as we go ... 3172 3173 u, err := store.StoreURL(api) 3174 c.Assert(err, IsNil) 3175 c.Check(u.String(), Matches, api.String()+".*") 3176 3177 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "https://force-api.local/"), IsNil) 3178 defer os.Setenv("SNAPPY_FORCE_API_URL", "") 3179 u, err = store.StoreURL(api) 3180 c.Assert(err, IsNil) 3181 c.Check(u.String(), Matches, "https://force-api.local/.*") 3182 3183 c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", "https://force-cpi.local/api/v1/"), IsNil) 3184 defer os.Setenv("SNAPPY_FORCE_CPI_URL", "") 3185 u, err = store.StoreURL(api) 3186 c.Assert(err, IsNil) 3187 c.Check(u.String(), Matches, "https://force-cpi.local/.*") 3188 } 3189 3190 func (s *storeTestSuite) TestStoreURLBadEnvironAPI(c *C) { 3191 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "://force-api.local/"), IsNil) 3192 defer os.Setenv("SNAPPY_FORCE_API_URL", "") 3193 _, err := store.StoreURL(store.ApiURL()) 3194 c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_API_URL: parse \"?://force-api.local/\"?: missing protocol scheme") 3195 } 3196 3197 func (s *storeTestSuite) TestStoreURLBadEnvironCPI(c *C) { 3198 c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", "://force-cpi.local/api/v1/"), IsNil) 3199 defer os.Setenv("SNAPPY_FORCE_CPI_URL", "") 3200 _, err := store.StoreURL(store.ApiURL()) 3201 c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_CPI_URL: parse \"?://force-cpi.local/\"?: missing protocol scheme") 3202 } 3203 3204 func (s *storeTestSuite) TestStoreDeveloperURLDependsOnEnviron(c *C) { 3205 defer snapdenv.MockUseStagingStore(false)() 3206 before := store.StoreDeveloperURL() 3207 3208 snapdenv.MockUseStagingStore(true) 3209 after := store.StoreDeveloperURL() 3210 3211 c.Check(before, Not(Equals), after) 3212 } 3213 3214 func (s *storeTestSuite) TestStoreDefaultConfig(c *C) { 3215 c.Check(store.DefaultConfig().StoreBaseURL.String(), Equals, "https://api.snapcraft.io/") 3216 c.Check(store.DefaultConfig().AssertionsBaseURL, IsNil) 3217 } 3218 3219 func (s *storeTestSuite) TestNew(c *C) { 3220 aStore := store.New(nil, nil) 3221 c.Assert(aStore, NotNil) 3222 // check for fields 3223 c.Check(aStore.DetailFields(), DeepEquals, store.DefaultConfig().DetailFields) 3224 } 3225 3226 func (s *storeTestSuite) TestSuggestedCurrency(c *C) { 3227 suggestedCurrency := "GBP" 3228 3229 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3230 assertRequest(c, r, "GET", infoPathPattern) 3231 w.Header().Set("X-Suggested-Currency", suggestedCurrency) 3232 w.WriteHeader(200) 3233 3234 io.WriteString(w, mockInfoJSON) 3235 })) 3236 3237 c.Assert(mockServer, NotNil) 3238 defer mockServer.Close() 3239 3240 mockServerURL, _ := url.Parse(mockServer.URL) 3241 cfg := store.Config{ 3242 StoreBaseURL: mockServerURL, 3243 } 3244 sto := store.New(&cfg, nil) 3245 3246 // the store doesn't know the currency until after the first search, so fall back to dollars 3247 c.Check(sto.SuggestedCurrency(), Equals, "USD") 3248 3249 // we should soon have a suggested currency 3250 spec := store.SnapSpec{ 3251 Name: "hello-world", 3252 } 3253 result, err := sto.SnapInfo(s.ctx, spec, nil) 3254 c.Assert(err, IsNil) 3255 c.Assert(result, NotNil) 3256 c.Check(sto.SuggestedCurrency(), Equals, "GBP") 3257 3258 suggestedCurrency = "EUR" 3259 3260 // checking the currency updates 3261 result, err = sto.SnapInfo(s.ctx, spec, nil) 3262 c.Assert(err, IsNil) 3263 c.Assert(result, NotNil) 3264 c.Check(sto.SuggestedCurrency(), Equals, "EUR") 3265 } 3266 3267 func (s *storeTestSuite) TestDecorateOrders(c *C) { 3268 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3269 assertRequest(c, r, "GET", ordersPath) 3270 // check device authorization is set, implicitly checking doRequest was used 3271 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3272 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3273 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3274 c.Check(r.URL.Path, Equals, ordersPath) 3275 io.WriteString(w, mockOrdersJSON) 3276 })) 3277 3278 c.Assert(mockPurchasesServer, NotNil) 3279 defer mockPurchasesServer.Close() 3280 3281 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3282 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3283 cfg := store.Config{ 3284 StoreBaseURL: mockServerURL, 3285 } 3286 sto := store.New(&cfg, dauthCtx) 3287 3288 helloWorld := &snap.Info{} 3289 helloWorld.SnapID = helloWorldSnapID 3290 helloWorld.Prices = map[string]float64{"USD": 1.23} 3291 helloWorld.Paid = true 3292 3293 funkyApp := &snap.Info{} 3294 funkyApp.SnapID = funkyAppSnapID 3295 funkyApp.Prices = map[string]float64{"USD": 2.34} 3296 funkyApp.Paid = true 3297 3298 otherApp := &snap.Info{} 3299 otherApp.SnapID = "other" 3300 otherApp.Prices = map[string]float64{"USD": 3.45} 3301 otherApp.Paid = true 3302 3303 otherApp2 := &snap.Info{} 3304 otherApp2.SnapID = "other2" 3305 3306 snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2} 3307 3308 err := sto.DecorateOrders(snaps, s.user) 3309 c.Assert(err, IsNil) 3310 3311 c.Check(helloWorld.MustBuy, Equals, false) 3312 c.Check(funkyApp.MustBuy, Equals, false) 3313 c.Check(otherApp.MustBuy, Equals, true) 3314 c.Check(otherApp2.MustBuy, Equals, false) 3315 } 3316 3317 func (s *storeTestSuite) TestDecorateOrdersFailedAccess(c *C) { 3318 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3319 assertRequest(c, r, "GET", ordersPath) 3320 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3321 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3322 c.Check(r.URL.Path, Equals, ordersPath) 3323 w.WriteHeader(401) 3324 io.WriteString(w, "{}") 3325 })) 3326 3327 c.Assert(mockPurchasesServer, NotNil) 3328 defer mockPurchasesServer.Close() 3329 3330 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3331 cfg := store.Config{ 3332 StoreBaseURL: mockServerURL, 3333 } 3334 sto := store.New(&cfg, nil) 3335 3336 helloWorld := &snap.Info{} 3337 helloWorld.SnapID = helloWorldSnapID 3338 helloWorld.Prices = map[string]float64{"USD": 1.23} 3339 helloWorld.Paid = true 3340 3341 funkyApp := &snap.Info{} 3342 funkyApp.SnapID = funkyAppSnapID 3343 funkyApp.Prices = map[string]float64{"USD": 2.34} 3344 funkyApp.Paid = true 3345 3346 otherApp := &snap.Info{} 3347 otherApp.SnapID = "other" 3348 otherApp.Prices = map[string]float64{"USD": 3.45} 3349 otherApp.Paid = true 3350 3351 otherApp2 := &snap.Info{} 3352 otherApp2.SnapID = "other2" 3353 3354 snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2} 3355 3356 err := sto.DecorateOrders(snaps, s.user) 3357 c.Assert(err, NotNil) 3358 3359 c.Check(helloWorld.MustBuy, Equals, true) 3360 c.Check(funkyApp.MustBuy, Equals, true) 3361 c.Check(otherApp.MustBuy, Equals, true) 3362 c.Check(otherApp2.MustBuy, Equals, false) 3363 } 3364 3365 func (s *storeTestSuite) TestDecorateOrdersNoAuth(c *C) { 3366 cfg := store.Config{} 3367 sto := store.New(&cfg, nil) 3368 3369 helloWorld := &snap.Info{} 3370 helloWorld.SnapID = helloWorldSnapID 3371 helloWorld.Prices = map[string]float64{"USD": 1.23} 3372 helloWorld.Paid = true 3373 3374 funkyApp := &snap.Info{} 3375 funkyApp.SnapID = funkyAppSnapID 3376 funkyApp.Prices = map[string]float64{"USD": 2.34} 3377 funkyApp.Paid = true 3378 3379 otherApp := &snap.Info{} 3380 otherApp.SnapID = "other" 3381 otherApp.Prices = map[string]float64{"USD": 3.45} 3382 otherApp.Paid = true 3383 3384 otherApp2 := &snap.Info{} 3385 otherApp2.SnapID = "other2" 3386 3387 snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2} 3388 3389 err := sto.DecorateOrders(snaps, nil) 3390 c.Assert(err, IsNil) 3391 3392 c.Check(helloWorld.MustBuy, Equals, true) 3393 c.Check(funkyApp.MustBuy, Equals, true) 3394 c.Check(otherApp.MustBuy, Equals, true) 3395 c.Check(otherApp2.MustBuy, Equals, false) 3396 } 3397 3398 func (s *storeTestSuite) TestDecorateOrdersAllFree(c *C) { 3399 requestRecieved := false 3400 3401 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3402 c.Error(r.URL.Path) 3403 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3404 requestRecieved = true 3405 io.WriteString(w, `{"orders": []}`) 3406 })) 3407 3408 c.Assert(mockPurchasesServer, NotNil) 3409 defer mockPurchasesServer.Close() 3410 3411 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3412 cfg := store.Config{ 3413 StoreBaseURL: mockServerURL, 3414 } 3415 3416 sto := store.New(&cfg, nil) 3417 3418 // This snap is free 3419 helloWorld := &snap.Info{} 3420 helloWorld.SnapID = helloWorldSnapID 3421 3422 // This snap is also free 3423 funkyApp := &snap.Info{} 3424 funkyApp.SnapID = funkyAppSnapID 3425 3426 snaps := []*snap.Info{helloWorld, funkyApp} 3427 3428 // There should be no request to the purchase server. 3429 err := sto.DecorateOrders(snaps, s.user) 3430 c.Assert(err, IsNil) 3431 c.Check(requestRecieved, Equals, false) 3432 } 3433 3434 func (s *storeTestSuite) TestDecorateOrdersSingle(c *C) { 3435 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3436 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3437 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3438 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3439 c.Check(r.URL.Path, Equals, ordersPath) 3440 io.WriteString(w, mockSingleOrderJSON) 3441 })) 3442 3443 c.Assert(mockPurchasesServer, NotNil) 3444 defer mockPurchasesServer.Close() 3445 3446 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3447 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3448 cfg := store.Config{ 3449 StoreBaseURL: mockServerURL, 3450 } 3451 sto := store.New(&cfg, dauthCtx) 3452 3453 helloWorld := &snap.Info{} 3454 helloWorld.SnapID = helloWorldSnapID 3455 helloWorld.Prices = map[string]float64{"USD": 1.23} 3456 helloWorld.Paid = true 3457 3458 snaps := []*snap.Info{helloWorld} 3459 3460 err := sto.DecorateOrders(snaps, s.user) 3461 c.Assert(err, IsNil) 3462 c.Check(helloWorld.MustBuy, Equals, false) 3463 } 3464 3465 func (s *storeTestSuite) TestDecorateOrdersSingleFreeSnap(c *C) { 3466 cfg := store.Config{} 3467 sto := store.New(&cfg, nil) 3468 3469 helloWorld := &snap.Info{} 3470 helloWorld.SnapID = helloWorldSnapID 3471 3472 snaps := []*snap.Info{helloWorld} 3473 3474 err := sto.DecorateOrders(snaps, s.user) 3475 c.Assert(err, IsNil) 3476 c.Check(helloWorld.MustBuy, Equals, false) 3477 } 3478 3479 func (s *storeTestSuite) TestDecorateOrdersSingleNotFound(c *C) { 3480 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3481 assertRequest(c, r, "GET", ordersPath) 3482 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3483 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3484 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3485 c.Check(r.URL.Path, Equals, ordersPath) 3486 w.WriteHeader(404) 3487 io.WriteString(w, "{}") 3488 })) 3489 3490 c.Assert(mockPurchasesServer, NotNil) 3491 defer mockPurchasesServer.Close() 3492 3493 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3494 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3495 cfg := store.Config{ 3496 StoreBaseURL: mockServerURL, 3497 } 3498 sto := store.New(&cfg, dauthCtx) 3499 3500 helloWorld := &snap.Info{} 3501 helloWorld.SnapID = helloWorldSnapID 3502 helloWorld.Prices = map[string]float64{"USD": 1.23} 3503 helloWorld.Paid = true 3504 3505 snaps := []*snap.Info{helloWorld} 3506 3507 err := sto.DecorateOrders(snaps, s.user) 3508 c.Assert(err, NotNil) 3509 c.Check(helloWorld.MustBuy, Equals, true) 3510 } 3511 3512 func (s *storeTestSuite) TestDecorateOrdersTokenExpired(c *C) { 3513 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3514 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3515 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3516 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3517 c.Check(r.URL.Path, Equals, ordersPath) 3518 w.WriteHeader(401) 3519 io.WriteString(w, "") 3520 })) 3521 3522 c.Assert(mockPurchasesServer, NotNil) 3523 defer mockPurchasesServer.Close() 3524 3525 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3526 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3527 cfg := store.Config{ 3528 StoreBaseURL: mockServerURL, 3529 } 3530 sto := store.New(&cfg, dauthCtx) 3531 3532 helloWorld := &snap.Info{} 3533 helloWorld.SnapID = helloWorldSnapID 3534 helloWorld.Prices = map[string]float64{"USD": 1.23} 3535 helloWorld.Paid = true 3536 3537 snaps := []*snap.Info{helloWorld} 3538 3539 err := sto.DecorateOrders(snaps, s.user) 3540 c.Assert(err, NotNil) 3541 c.Check(helloWorld.MustBuy, Equals, true) 3542 } 3543 3544 func (s *storeTestSuite) TestMustBuy(c *C) { 3545 // Never need to buy a free snap. 3546 c.Check(store.MustBuy(false, true), Equals, false) 3547 c.Check(store.MustBuy(false, false), Equals, false) 3548 3549 // Don't need to buy snaps that have been bought. 3550 c.Check(store.MustBuy(true, true), Equals, false) 3551 3552 // Need to buy snaps that aren't bought. 3553 c.Check(store.MustBuy(true, false), Equals, true) 3554 } 3555 3556 var buyTests = []struct { 3557 suggestedCurrency string 3558 expectedInput string 3559 buyStatus int 3560 buyResponse string 3561 buyErrorMessage string 3562 buyErrorCode string 3563 snapID string 3564 price float64 3565 currency string 3566 expectedResult *client.BuyResult 3567 expectedError string 3568 }{ 3569 { 3570 // successful buying 3571 suggestedCurrency: "EUR", 3572 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"0.99","currency":"EUR"}`, 3573 buyResponse: mockOrderResponseJSON, 3574 expectedResult: &client.BuyResult{State: "Complete"}, 3575 }, 3576 { 3577 // failure due to invalid price 3578 suggestedCurrency: "USD", 3579 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"5.99","currency":"USD"}`, 3580 buyStatus: 400, 3581 buyErrorCode: "invalid-field", 3582 buyErrorMessage: "invalid price specified", 3583 price: 5.99, 3584 expectedError: "cannot buy snap: bad request: invalid price specified", 3585 }, 3586 { 3587 // failure due to unknown snap ID 3588 suggestedCurrency: "USD", 3589 expectedInput: `{"snap_id":"invalid snap ID","amount":"0.99","currency":"EUR"}`, 3590 buyStatus: 404, 3591 buyErrorCode: "not-found", 3592 buyErrorMessage: "Snap package not found", 3593 snapID: "invalid snap ID", 3594 price: 0.99, 3595 currency: "EUR", 3596 expectedError: "cannot buy snap: server says not found: Snap package not found", 3597 }, 3598 { 3599 // failure due to "Purchase failed" 3600 suggestedCurrency: "USD", 3601 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`, 3602 buyStatus: 402, // Payment Required 3603 buyErrorCode: "request-failed", 3604 buyErrorMessage: "Purchase failed", 3605 expectedError: "payment declined", 3606 }, 3607 { 3608 // failure due to no payment methods 3609 suggestedCurrency: "USD", 3610 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`, 3611 buyStatus: 403, 3612 buyErrorCode: "no-payment-methods", 3613 buyErrorMessage: "No payment methods associated with your account.", 3614 expectedError: "no payment methods", 3615 }, 3616 { 3617 // failure due to terms of service not accepted 3618 suggestedCurrency: "USD", 3619 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`, 3620 buyStatus: 403, 3621 buyErrorCode: "tos-not-accepted", 3622 buyErrorMessage: "You must accept the latest terms of service first.", 3623 expectedError: "terms of service not accepted", 3624 }, 3625 } 3626 3627 func (s *storeTestSuite) TestBuy500(c *C) { 3628 n := 0 3629 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3630 switch r.URL.Path { 3631 case detailsPath("hello-world"): 3632 n++ 3633 w.WriteHeader(500) 3634 case buyPath: 3635 case customersMePath: 3636 // default 200 response 3637 default: 3638 c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path) 3639 } 3640 })) 3641 c.Assert(mockServer, NotNil) 3642 defer mockServer.Close() 3643 3644 mockServerURL, _ := url.Parse(mockServer.URL) 3645 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3646 cfg := store.Config{ 3647 StoreBaseURL: mockServerURL, 3648 } 3649 sto := store.New(&cfg, dauthCtx) 3650 3651 buyOptions := &client.BuyOptions{ 3652 SnapID: helloWorldSnapID, 3653 Currency: "USD", 3654 Price: 1, 3655 } 3656 _, err := sto.Buy(buyOptions, s.user) 3657 c.Assert(err, NotNil) 3658 } 3659 3660 func (s *storeTestSuite) TestBuy(c *C) { 3661 for _, test := range buyTests { 3662 searchServerCalled := false 3663 purchaseServerGetCalled := false 3664 purchaseServerPostCalled := false 3665 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3666 switch r.URL.Path { 3667 case infoPath("hello-world"): 3668 c.Assert(r.Method, Equals, "GET") 3669 w.Header().Set("Content-Type", "application/json") 3670 w.Header().Set("X-Suggested-Currency", test.suggestedCurrency) 3671 w.WriteHeader(200) 3672 io.WriteString(w, mockInfoJSON) 3673 searchServerCalled = true 3674 case ordersPath: 3675 c.Assert(r.Method, Equals, "GET") 3676 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3677 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3678 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3679 io.WriteString(w, `{"orders": []}`) 3680 purchaseServerGetCalled = true 3681 case buyPath: 3682 c.Assert(r.Method, Equals, "POST") 3683 // check device authorization is set, implicitly checking doRequest was used 3684 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3685 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3686 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3687 c.Check(r.Header.Get("Content-Type"), Equals, store.JsonContentType) 3688 c.Check(r.URL.Path, Equals, buyPath) 3689 jsonReq, err := ioutil.ReadAll(r.Body) 3690 c.Assert(err, IsNil) 3691 c.Check(string(jsonReq), Equals, test.expectedInput) 3692 if test.buyErrorCode == "" { 3693 io.WriteString(w, test.buyResponse) 3694 } else { 3695 w.WriteHeader(test.buyStatus) 3696 // TODO(matt): this is fugly! 3697 fmt.Fprintf(w, ` 3698 { 3699 "error_list": [ 3700 { 3701 "code": "%s", 3702 "message": "%s" 3703 } 3704 ] 3705 }`, test.buyErrorCode, test.buyErrorMessage) 3706 } 3707 3708 purchaseServerPostCalled = true 3709 default: 3710 c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path) 3711 } 3712 })) 3713 c.Assert(mockServer, NotNil) 3714 defer mockServer.Close() 3715 3716 mockServerURL, _ := url.Parse(mockServer.URL) 3717 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3718 cfg := store.Config{ 3719 StoreBaseURL: mockServerURL, 3720 } 3721 sto := store.New(&cfg, dauthCtx) 3722 3723 // Find the snap first 3724 spec := store.SnapSpec{ 3725 Name: "hello-world", 3726 } 3727 snap, err := sto.SnapInfo(s.ctx, spec, s.user) 3728 c.Assert(snap, NotNil) 3729 c.Assert(err, IsNil) 3730 3731 buyOptions := &client.BuyOptions{ 3732 SnapID: snap.SnapID, 3733 Currency: sto.SuggestedCurrency(), 3734 Price: snap.Prices[sto.SuggestedCurrency()], 3735 } 3736 if test.snapID != "" { 3737 buyOptions.SnapID = test.snapID 3738 } 3739 if test.currency != "" { 3740 buyOptions.Currency = test.currency 3741 } 3742 if test.price > 0 { 3743 buyOptions.Price = test.price 3744 } 3745 result, err := sto.Buy(buyOptions, s.user) 3746 3747 c.Check(result, DeepEquals, test.expectedResult) 3748 if test.expectedError == "" { 3749 c.Check(err, IsNil) 3750 } else { 3751 c.Assert(err, NotNil) 3752 c.Check(err.Error(), Equals, test.expectedError) 3753 } 3754 3755 c.Check(searchServerCalled, Equals, true) 3756 c.Check(purchaseServerGetCalled, Equals, true) 3757 c.Check(purchaseServerPostCalled, Equals, true) 3758 } 3759 } 3760 3761 func (s *storeTestSuite) TestBuyFailArgumentChecking(c *C) { 3762 sto := store.New(&store.Config{}, nil) 3763 3764 // no snap ID 3765 result, err := sto.Buy(&client.BuyOptions{ 3766 Price: 1.0, 3767 Currency: "USD", 3768 }, s.user) 3769 c.Assert(result, IsNil) 3770 c.Assert(err, NotNil) 3771 c.Check(err.Error(), Equals, "cannot buy snap: snap ID missing") 3772 3773 // no price 3774 result, err = sto.Buy(&client.BuyOptions{ 3775 SnapID: "snap ID", 3776 Currency: "USD", 3777 }, s.user) 3778 c.Assert(result, IsNil) 3779 c.Assert(err, NotNil) 3780 c.Check(err.Error(), Equals, "cannot buy snap: invalid expected price") 3781 3782 // no currency 3783 result, err = sto.Buy(&client.BuyOptions{ 3784 SnapID: "snap ID", 3785 Price: 1.0, 3786 }, s.user) 3787 c.Assert(result, IsNil) 3788 c.Assert(err, NotNil) 3789 c.Check(err.Error(), Equals, "cannot buy snap: currency missing") 3790 3791 // no user 3792 result, err = sto.Buy(&client.BuyOptions{ 3793 SnapID: "snap ID", 3794 Price: 1.0, 3795 Currency: "USD", 3796 }, nil) 3797 c.Assert(result, IsNil) 3798 c.Assert(err, NotNil) 3799 c.Check(err.Error(), Equals, "you need to log in first") 3800 } 3801 3802 var readyToBuyTests = []struct { 3803 Input func(w http.ResponseWriter) 3804 Test func(c *C, err error) 3805 NumOfCalls int 3806 }{ 3807 { 3808 // A user account the is ready for buying 3809 Input: func(w http.ResponseWriter) { 3810 io.WriteString(w, ` 3811 { 3812 "latest_tos_date": "2016-09-14T00:00:00+00:00", 3813 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3814 "latest_tos_accepted": true, 3815 "has_payment_method": true 3816 } 3817 `) 3818 }, 3819 Test: func(c *C, err error) { 3820 c.Check(err, IsNil) 3821 }, 3822 NumOfCalls: 1, 3823 }, 3824 { 3825 // A user account that hasn't accepted the TOS 3826 Input: func(w http.ResponseWriter) { 3827 io.WriteString(w, ` 3828 { 3829 "latest_tos_date": "2016-10-14T00:00:00+00:00", 3830 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3831 "latest_tos_accepted": false, 3832 "has_payment_method": true 3833 } 3834 `) 3835 }, 3836 Test: func(c *C, err error) { 3837 c.Assert(err, NotNil) 3838 c.Check(err.Error(), Equals, "terms of service not accepted") 3839 }, 3840 NumOfCalls: 1, 3841 }, 3842 { 3843 // A user account that has no payment method 3844 Input: func(w http.ResponseWriter) { 3845 io.WriteString(w, ` 3846 { 3847 "latest_tos_date": "2016-10-14T00:00:00+00:00", 3848 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3849 "latest_tos_accepted": true, 3850 "has_payment_method": false 3851 } 3852 `) 3853 }, 3854 Test: func(c *C, err error) { 3855 c.Assert(err, NotNil) 3856 c.Check(err.Error(), Equals, "no payment methods") 3857 }, 3858 NumOfCalls: 1, 3859 }, 3860 { 3861 // A user account that has no payment method and has not accepted the TOS 3862 Input: func(w http.ResponseWriter) { 3863 io.WriteString(w, ` 3864 { 3865 "latest_tos_date": "2016-10-14T00:00:00+00:00", 3866 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3867 "latest_tos_accepted": false, 3868 "has_payment_method": false 3869 } 3870 `) 3871 }, 3872 Test: func(c *C, err error) { 3873 c.Assert(err, NotNil) 3874 c.Check(err.Error(), Equals, "no payment methods") 3875 }, 3876 NumOfCalls: 1, 3877 }, 3878 { 3879 // No user account exists 3880 Input: func(w http.ResponseWriter) { 3881 w.WriteHeader(404) 3882 io.WriteString(w, "{}") 3883 }, 3884 Test: func(c *C, err error) { 3885 c.Assert(err, NotNil) 3886 c.Check(err.Error(), Equals, "cannot get customer details: server says no account exists") 3887 }, 3888 NumOfCalls: 1, 3889 }, 3890 { 3891 // An unknown set of errors occurs 3892 Input: func(w http.ResponseWriter) { 3893 w.WriteHeader(500) 3894 io.WriteString(w, ` 3895 { 3896 "error_list": [ 3897 { 3898 "code": "code 1", 3899 "message": "message 1" 3900 }, 3901 { 3902 "code": "code 2", 3903 "message": "message 2" 3904 } 3905 ] 3906 }`) 3907 }, 3908 Test: func(c *C, err error) { 3909 c.Assert(err, NotNil) 3910 c.Check(err.Error(), Equals, `message 1`) 3911 }, 3912 NumOfCalls: 5, 3913 }, 3914 } 3915 3916 func (s *storeTestSuite) TestReadyToBuy(c *C) { 3917 for _, test := range readyToBuyTests { 3918 purchaseServerGetCalled := 0 3919 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3920 assertRequest(c, r, "GET", customersMePath) 3921 switch r.Method { 3922 case "GET": 3923 // check device authorization is set, implicitly checking doRequest was used 3924 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3925 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3926 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3927 c.Check(r.URL.Path, Equals, customersMePath) 3928 test.Input(w) 3929 purchaseServerGetCalled++ 3930 default: 3931 c.Error("Unexpected request method: ", r.Method) 3932 } 3933 })) 3934 3935 c.Assert(mockPurchasesServer, NotNil) 3936 defer mockPurchasesServer.Close() 3937 3938 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3939 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3940 cfg := store.Config{ 3941 StoreBaseURL: mockServerURL, 3942 } 3943 sto := store.New(&cfg, dauthCtx) 3944 3945 err := sto.ReadyToBuy(s.user) 3946 test.Test(c, err) 3947 c.Check(purchaseServerGetCalled, Equals, test.NumOfCalls) 3948 } 3949 } 3950 3951 func (s *storeTestSuite) TestDoRequestSetRangeHeaderOnRedirect(c *C) { 3952 n := 0 3953 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3954 switch n { 3955 case 0: 3956 http.Redirect(w, r, r.URL.Path+"-else", 302) 3957 n++ 3958 case 1: 3959 c.Check(r.URL.Path, Equals, "/somewhere-else") 3960 rg := r.Header.Get("Range") 3961 c.Check(rg, Equals, "bytes=5-") 3962 default: 3963 panic("got more than 2 requests in this test") 3964 } 3965 })) 3966 3967 c.Assert(mockServer, NotNil) 3968 defer mockServer.Close() 3969 3970 url, err := url.Parse(mockServer.URL + "/somewhere") 3971 c.Assert(err, IsNil) 3972 reqOptions := store.NewRequestOptions("GET", url) 3973 reqOptions.ExtraHeaders = map[string]string{ 3974 "Range": "bytes=5-", 3975 } 3976 3977 sto := store.New(&store.Config{}, nil) 3978 _, err = sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 3979 c.Assert(err, IsNil) 3980 } 3981 3982 func (s *storeTestSuite) TestConnectivityCheckHappy(c *C) { 3983 seenPaths := make(map[string]int, 2) 3984 var mockServerURL *url.URL 3985 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3986 switch r.URL.Path { 3987 case "/v2/snaps/info/core": 3988 c.Check(r.Method, Equals, "GET") 3989 c.Check(r.URL.Query(), DeepEquals, url.Values{"fields": {"download"}, "architecture": {arch.DpkgArchitecture()}}) 3990 u, err := url.Parse("/download/core") 3991 c.Assert(err, IsNil) 3992 io.WriteString(w, 3993 fmt.Sprintf(`{"channel-map": [{"download": {"url": %q}}, {"download": {"url": %q}}, {"download": {"url": %q}}]}`, 3994 mockServerURL.ResolveReference(u).String(), 3995 mockServerURL.String()+"/bogus1/", 3996 mockServerURL.String()+"/bogus2/", 3997 )) 3998 case "/download/core": 3999 c.Check(r.Method, Equals, "HEAD") 4000 w.WriteHeader(200) 4001 default: 4002 c.Fatalf("unexpected request: %s", r.URL.String()) 4003 return 4004 } 4005 seenPaths[r.URL.Path]++ 4006 })) 4007 c.Assert(mockServer, NotNil) 4008 defer mockServer.Close() 4009 mockServerURL, _ = url.Parse(mockServer.URL) 4010 4011 sto := store.New(&store.Config{ 4012 StoreBaseURL: mockServerURL, 4013 }, nil) 4014 connectivity, err := sto.ConnectivityCheck() 4015 c.Assert(err, IsNil) 4016 // everything is the test server, here 4017 c.Check(connectivity, DeepEquals, map[string]bool{ 4018 mockServerURL.Host: true, 4019 }) 4020 c.Check(seenPaths, DeepEquals, map[string]int{ 4021 "/v2/snaps/info/core": 1, 4022 "/download/core": 1, 4023 }) 4024 } 4025 4026 func (s *storeTestSuite) TestConnectivityCheckUnhappy(c *C) { 4027 store.MockConnCheckStrategy(&s.BaseTest, retry.LimitCount(3, retry.Exponential{ 4028 Initial: time.Millisecond, 4029 Factor: 1.3, 4030 })) 4031 4032 seenPaths := make(map[string]int, 2) 4033 var mockServerURL *url.URL 4034 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4035 switch r.URL.Path { 4036 case "/v2/snaps/info/core": 4037 w.WriteHeader(500) 4038 default: 4039 c.Fatalf("unexpected request: %s", r.URL.String()) 4040 return 4041 } 4042 seenPaths[r.URL.Path]++ 4043 })) 4044 c.Assert(mockServer, NotNil) 4045 defer mockServer.Close() 4046 mockServerURL, _ = url.Parse(mockServer.URL) 4047 4048 sto := store.New(&store.Config{ 4049 StoreBaseURL: mockServerURL, 4050 }, nil) 4051 connectivity, err := sto.ConnectivityCheck() 4052 c.Assert(err, IsNil) 4053 // everything is the test server, here 4054 c.Check(connectivity, DeepEquals, map[string]bool{ 4055 mockServerURL.Host: false, 4056 }) 4057 // three because retries 4058 c.Check(seenPaths, DeepEquals, map[string]int{ 4059 "/v2/snaps/info/core": 3, 4060 }) 4061 } 4062 4063 func (s *storeTestSuite) TestCreateCohort(c *C) { 4064 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4065 assertRequest(c, r, "POST", cohortsPath) 4066 // check device authorization is set, implicitly checking doRequest was used 4067 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 4068 4069 dec := json.NewDecoder(r.Body) 4070 var req struct { 4071 Snaps []string 4072 } 4073 err := dec.Decode(&req) 4074 c.Assert(err, IsNil) 4075 c.Check(dec.More(), Equals, false) 4076 4077 c.Check(req.Snaps, DeepEquals, []string{"foo", "bar"}) 4078 4079 io.WriteString(w, `{ 4080 "cohort-keys": { 4081 "potato": "U3VwZXIgc2VjcmV0IHN0dWZmIGVuY3J5cHRlZCBoZXJlLg==" 4082 } 4083 }`) 4084 })) 4085 4086 c.Assert(mockServer, NotNil) 4087 defer mockServer.Close() 4088 4089 mockServerURL, _ := url.Parse(mockServer.URL) 4090 cfg := store.Config{ 4091 StoreBaseURL: mockServerURL, 4092 } 4093 dauthCtx := &testDauthContext{c: c, device: s.device} 4094 sto := store.New(&cfg, dauthCtx) 4095 4096 cohorts, err := sto.CreateCohorts(s.ctx, []string{"foo", "bar"}) 4097 c.Assert(err, IsNil) 4098 c.Assert(cohorts, DeepEquals, map[string]string{ 4099 "potato": "U3VwZXIgc2VjcmV0IHN0dWZmIGVuY3J5cHRlZCBoZXJlLg==", 4100 }) 4101 }