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