github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/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 const mockNamesJSON = ` 2288 { 2289 "_embedded": { 2290 "clickindex:package": [ 2291 { 2292 "aliases": [ 2293 { 2294 "name": "potato", 2295 "target": "baz" 2296 }, 2297 { 2298 "name": "meh", 2299 "target": "baz" 2300 } 2301 ], 2302 "apps": ["baz"], 2303 "title": "a title", 2304 "summary": "oneary plus twoary", 2305 "package_name": "bar", 2306 "version": "2.0" 2307 }, 2308 { 2309 "aliases": [{"name": "meh", "target": "foo"}], 2310 "apps": ["foo"], 2311 "package_name": "foo", 2312 "version": "1.0" 2313 } 2314 ] 2315 } 2316 }` 2317 2318 func (s *storeTestSuite) TestSnapCommandsOnClassic(c *C) { 2319 s.testSnapCommands(c, true) 2320 } 2321 2322 func (s *storeTestSuite) TestSnapCommandsOnCore(c *C) { 2323 s.testSnapCommands(c, false) 2324 } 2325 2326 func (s *storeTestSuite) testSnapCommands(c *C, onClassic bool) { 2327 c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) 2328 defer release.MockOnClassic(onClassic)() 2329 2330 n := 0 2331 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2332 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 2333 2334 switch n { 2335 case 0: 2336 query := r.URL.Query() 2337 c.Check(query, HasLen, 1) 2338 expectedConfinement := "strict" 2339 if onClassic { 2340 expectedConfinement = "strict,classic" 2341 } 2342 c.Check(query.Get("confinement"), Equals, expectedConfinement) 2343 c.Check(r.URL.Path, Equals, "/api/v1/snaps/names") 2344 default: 2345 c.Fatalf("what? %d", n) 2346 } 2347 2348 w.Header().Set("Content-Type", "application/hal+json") 2349 w.Header().Set("Content-Length", fmt.Sprint(len(mockNamesJSON))) 2350 w.WriteHeader(200) 2351 io.WriteString(w, mockNamesJSON) 2352 n++ 2353 })) 2354 c.Assert(mockServer, NotNil) 2355 defer mockServer.Close() 2356 2357 serverURL, _ := url.Parse(mockServer.URL) 2358 dauthCtx := &testDauthContext{c: c, device: s.device} 2359 sto := store.New(&store.Config{StoreBaseURL: serverURL}, dauthCtx) 2360 2361 db, err := advisor.Create() 2362 c.Assert(err, IsNil) 2363 defer db.Rollback() 2364 2365 var bufNames bytes.Buffer 2366 err = sto.WriteCatalogs(s.ctx, &bufNames, db) 2367 c.Assert(err, IsNil) 2368 db.Commit() 2369 c.Check(bufNames.String(), Equals, "bar\nfoo\n") 2370 2371 dump, err := advisor.DumpCommands() 2372 c.Assert(err, IsNil) 2373 c.Check(dump, DeepEquals, map[string]string{ 2374 "foo": `[{"snap":"foo","version":"1.0"}]`, 2375 "bar.baz": `[{"snap":"bar","version":"2.0"}]`, 2376 "potato": `[{"snap":"bar","version":"2.0"}]`, 2377 "meh": `[{"snap":"bar","version":"2.0"},{"snap":"foo","version":"1.0"}]`, 2378 }) 2379 c.Check(n, Equals, 1) 2380 } 2381 2382 func (s *storeTestSuite) TestSnapCommandsTooMany(c *C) { 2383 c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) 2384 2385 n := 0 2386 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2387 c.Check(r.Header.Get("X-Device-Authorization"), Equals, "") 2388 2389 switch n { 2390 case 0: 2391 c.Check(r.URL.Path, Equals, "/api/v1/snaps/names") 2392 default: 2393 c.Fatalf("what? %d", n) 2394 } 2395 2396 w.WriteHeader(429) 2397 n++ 2398 })) 2399 c.Assert(mockServer, NotNil) 2400 defer mockServer.Close() 2401 2402 serverURL, _ := url.Parse(mockServer.URL) 2403 dauthCtx := &testDauthContext{c: c, device: s.device} 2404 sto := store.New(&store.Config{StoreBaseURL: serverURL}, dauthCtx) 2405 2406 db, err := advisor.Create() 2407 c.Assert(err, IsNil) 2408 defer db.Rollback() 2409 2410 var bufNames bytes.Buffer 2411 err = sto.WriteCatalogs(s.ctx, &bufNames, db) 2412 c.Assert(err, Equals, store.ErrTooManyRequests) 2413 db.Commit() 2414 c.Check(bufNames.String(), Equals, "") 2415 2416 dump, err := advisor.DumpCommands() 2417 c.Assert(err, IsNil) 2418 c.Check(dump, HasLen, 0) 2419 c.Check(n, Equals, 1) 2420 } 2421 2422 func (s *storeTestSuite) testFind(c *C, apiV1 bool) { 2423 restore := release.MockOnClassic(false) 2424 defer restore() 2425 2426 var v1Fallback, v2Hit bool 2427 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2428 if apiV1 { 2429 if strings.Contains(r.URL.Path, findPath) { 2430 forceSearchV1(w) 2431 return 2432 } 2433 v1Fallback = true 2434 assertRequest(c, r, "GET", searchPath) 2435 } else { 2436 v2Hit = true 2437 assertRequest(c, r, "GET", findPath) 2438 } 2439 query := r.URL.Query() 2440 2441 q := query.Get("q") 2442 c.Check(q, Equals, "hello") 2443 2444 c.Check(r.UserAgent(), Equals, userAgent) 2445 2446 if apiV1 { 2447 // check device authorization is set, implicitly checking doRequest was used 2448 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2449 2450 // no store ID by default 2451 storeID := r.Header.Get("X-Ubuntu-Store") 2452 c.Check(storeID, Equals, "") 2453 2454 c.Check(r.URL.Query().Get("fields"), Equals, "abc,def") 2455 2456 c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, release.Series) 2457 c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, arch.DpkgArchitecture()) 2458 c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "false") 2459 2460 c.Check(r.Header.Get("X-Ubuntu-Confinement"), Equals, "") 2461 2462 w.Header().Set("X-Suggested-Currency", "GBP") 2463 2464 w.Header().Set("Content-Type", "application/hal+json") 2465 w.WriteHeader(200) 2466 2467 io.WriteString(w, MockSearchJSON) 2468 } else { 2469 2470 // check device authorization is set, implicitly checking doRequest was used 2471 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 2472 2473 // no store ID by default 2474 storeID := r.Header.Get("Snap-Device-Store") 2475 c.Check(storeID, Equals, "") 2476 2477 c.Check(r.URL.Query().Get("fields"), Equals, "abc,def") 2478 2479 c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series) 2480 c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture()) 2481 c.Check(r.Header.Get("Snap-Classic"), Equals, "false") 2482 2483 w.Header().Set("X-Suggested-Currency", "GBP") 2484 2485 w.Header().Set("Content-Type", "application/json") 2486 w.WriteHeader(200) 2487 2488 io.WriteString(w, MockSearchJSONv2) 2489 } 2490 })) 2491 2492 c.Assert(mockServer, NotNil) 2493 defer mockServer.Close() 2494 2495 mockServerURL, _ := url.Parse(mockServer.URL) 2496 cfg := store.Config{ 2497 StoreBaseURL: mockServerURL, 2498 DetailFields: []string{"abc", "def"}, 2499 FindFields: []string{"abc", "def"}, 2500 } 2501 2502 dauthCtx := &testDauthContext{c: c, device: s.device} 2503 sto := store.New(&cfg, dauthCtx) 2504 2505 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2506 c.Assert(err, IsNil) 2507 c.Assert(snaps, HasLen, 1) 2508 snp := snaps[0] 2509 c.Check(snp.InstanceName(), Equals, "hello-world") 2510 c.Check(snp.Revision, Equals, snap.R(27)) 2511 c.Check(snp.SnapID, Equals, helloWorldSnapID) 2512 c.Check(snp.Publisher, Equals, snap.StoreAccount{ 2513 ID: "canonical", 2514 Username: "canonical", 2515 DisplayName: "Canonical", 2516 Validation: "verified", 2517 }) 2518 c.Check(snp.Version, Equals, "6.3") 2519 c.Check(snp.Size, Equals, int64(20480)) 2520 c.Check(snp.Channel, Equals, "stable") 2521 c.Check(snp.Description(), Equals, "This is a simple hello world example.") 2522 c.Check(snp.Summary(), Equals, "The 'hello-world' of snaps") 2523 c.Check(snp.Title(), Equals, "This Is The Most Fantastical Snap of He…") 2524 c.Check(snp.License, Equals, "MIT") 2525 // this is more a "we know this isn't there" than an actual test for a wanted feature 2526 // NOTE snap.Epoch{} (which prints as "0", and is thus Unset) is not a valid Epoch. 2527 c.Check(snp.Epoch, DeepEquals, snap.Epoch{}) 2528 c.Assert(snp.Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49}) 2529 c.Assert(snp.Paid, Equals, true) 2530 c.Assert(snp.Media, DeepEquals, snap.MediaInfos{ 2531 { 2532 Type: "icon", 2533 URL: "https://dashboard.snapcraft.io/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png", 2534 }, { 2535 Type: "screenshot", 2536 URL: "https://dashboard.snapcraft.io/site_media/appmedia/2018/06/Screenshot_from_2018-06-14_09-33-31.png", 2537 }, 2538 }) 2539 c.Check(snp.MustBuy, Equals, true) 2540 c.Check(snp.Contact, Equals, "mailto:snappy-devel@lists.ubuntu.com") 2541 c.Check(snp.Base, Equals, "bare-base") 2542 2543 // Make sure the epoch (currently not sent by the store) defaults to "0" 2544 c.Check(snp.Epoch.String(), Equals, "0") 2545 2546 c.Check(sto.SuggestedCurrency(), Equals, "GBP") 2547 2548 if apiV1 { 2549 c.Check(snp.Architectures, DeepEquals, []string{"all"}) 2550 c.Check(snp.Sha3_384, Matches, `[[:xdigit:]]{96}`) 2551 c.Check(v1Fallback, Equals, true) 2552 } else { 2553 c.Check(snp.Website, Equals, "https://ubuntu.com") 2554 c.Check(snp.StoreURL, Equals, "https://snapcraft.io/hello-world") 2555 c.Check(snp.CommonIDs, DeepEquals, []string{"aaa", "bbb"}) 2556 c.Check(v2Hit, Equals, true) 2557 } 2558 } 2559 2560 func (s *storeTestSuite) TestFindV1(c *C) { 2561 apiV1 := true 2562 s.testFind(c, apiV1) 2563 } 2564 2565 func (s *storeTestSuite) TestFindV2(c *C) { 2566 s.testFind(c, false) 2567 } 2568 2569 func (s *storeTestSuite) TestFindV2FindFields(c *C) { 2570 dauthCtx := &testDauthContext{c: c, device: s.device} 2571 sto := store.New(nil, dauthCtx) 2572 2573 findFields := sto.FindFields() 2574 sort.Strings(findFields) 2575 c.Assert(findFields, DeepEquals, []string{ 2576 "base", "channel", "common-ids", "confinement", "contact", 2577 "description", "download", "license", "media", "prices", "private", 2578 "publisher", "revision", "store-url", "summary", "title", "type", 2579 "version", "website"}) 2580 } 2581 2582 func (s *storeTestSuite) testFindPrivate(c *C, apiV1 bool) { 2583 n := 0 2584 var v1Fallback, v2Hit bool 2585 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2586 if apiV1 { 2587 if strings.Contains(r.URL.Path, findPath) { 2588 forceSearchV1(w) 2589 return 2590 } 2591 v1Fallback = true 2592 assertRequest(c, r, "GET", searchPath) 2593 } else { 2594 v2Hit = true 2595 assertRequest(c, r, "GET", findPath) 2596 } 2597 2598 query := r.URL.Query() 2599 name := query.Get("name") 2600 q := query.Get("q") 2601 2602 switch n { 2603 case 0: 2604 if apiV1 { 2605 c.Check(r.URL.Path, Matches, ".*/search") 2606 } else { 2607 c.Check(r.URL.Path, Matches, ".*/find") 2608 } 2609 c.Check(name, Equals, "") 2610 c.Check(q, Equals, "foo") 2611 c.Check(query.Get("private"), Equals, "true") 2612 case 1: 2613 if apiV1 { 2614 c.Check(r.URL.Path, Matches, ".*/search") 2615 } else { 2616 c.Check(r.URL.Path, Matches, ".*/find") 2617 } 2618 c.Check(name, Equals, "foo") 2619 c.Check(q, Equals, "") 2620 c.Check(query.Get("private"), Equals, "true") 2621 default: 2622 c.Fatalf("what? %d", n) 2623 } 2624 2625 if apiV1 { 2626 w.Header().Set("Content-Type", "application/hal+json") 2627 w.WriteHeader(200) 2628 io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1)) 2629 2630 } else { 2631 w.Header().Set("Content-Type", "application/json") 2632 w.WriteHeader(200) 2633 io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": "2.99", "USD": "3.49"`, "", -1)) 2634 } 2635 2636 n++ 2637 })) 2638 c.Assert(mockServer, NotNil) 2639 defer mockServer.Close() 2640 2641 serverURL, _ := url.Parse(mockServer.URL) 2642 cfg := store.Config{ 2643 StoreBaseURL: serverURL, 2644 } 2645 2646 sto := store.New(&cfg, nil) 2647 2648 _, err := sto.Find(s.ctx, &store.Search{Query: "foo", Private: true}, s.user) 2649 c.Check(err, IsNil) 2650 2651 _, err = sto.Find(s.ctx, &store.Search{Query: "foo", Prefix: true, Private: true}, s.user) 2652 c.Check(err, IsNil) 2653 2654 _, err = sto.Find(s.ctx, &store.Search{Query: "foo", Private: true}, nil) 2655 c.Check(err, Equals, store.ErrUnauthenticated) 2656 2657 _, err = sto.Find(s.ctx, &store.Search{Query: "name:foo", Private: true}, s.user) 2658 c.Check(err, Equals, store.ErrBadQuery) 2659 2660 c.Check(n, Equals, 2) 2661 2662 if apiV1 { 2663 c.Check(v1Fallback, Equals, true) 2664 } else { 2665 c.Check(v2Hit, Equals, true) 2666 } 2667 } 2668 2669 func (s *storeTestSuite) TestFindV1Private(c *C) { 2670 apiV1 := true 2671 s.testFindPrivate(c, apiV1) 2672 } 2673 2674 func (s *storeTestSuite) TestFindV2Private(c *C) { 2675 s.testFindPrivate(c, false) 2676 } 2677 2678 func (s *storeTestSuite) TestFindV2ErrorList(c *C) { 2679 const errJSON = `{ 2680 "error-list": [ 2681 { 2682 "code": "api-error", 2683 "message": "api error occurred" 2684 } 2685 ] 2686 }` 2687 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2688 assertRequest(c, r, "GET", findPath) 2689 w.Header().Set("Content-Type", "application/json") 2690 w.WriteHeader(400) 2691 io.WriteString(w, errJSON) 2692 })) 2693 c.Assert(mockServer, NotNil) 2694 defer mockServer.Close() 2695 2696 mockServerURL, _ := url.Parse(mockServer.URL) 2697 cfg := store.Config{ 2698 StoreBaseURL: mockServerURL, 2699 FindFields: []string{}, 2700 } 2701 sto := store.New(&cfg, nil) 2702 _, err := sto.Find(s.ctx, &store.Search{Query: "x"}, nil) 2703 c.Check(err, ErrorMatches, `api error occurred`) 2704 } 2705 2706 func (s *storeTestSuite) TestFindFailures(c *C) { 2707 // bad query check is done early in Find(), so the test covers both search 2708 // v1 & v2 2709 sto := store.New(&store.Config{StoreBaseURL: new(url.URL)}, nil) 2710 _, err := sto.Find(s.ctx, &store.Search{Query: "foo:bar"}, nil) 2711 c.Check(err, Equals, store.ErrBadQuery) 2712 } 2713 2714 func (s *storeTestSuite) TestFindInvalidScope(c *C) { 2715 // bad query check is done early in Find(), so the test covers both search 2716 // v1 & v2 2717 sto := store.New(&store.Config{StoreBaseURL: new(url.URL)}, nil) 2718 _, err := sto.Find(s.ctx, &store.Search{Query: "", Scope: "foo"}, nil) 2719 c.Check(err, Equals, store.ErrInvalidScope) 2720 } 2721 2722 func (s *storeTestSuite) testFindFails(c *C, apiV1 bool) { 2723 var v1Fallback, v2Hit bool 2724 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2725 if apiV1 { 2726 if strings.Contains(r.URL.Path, findPath) { 2727 forceSearchV1(w) 2728 return 2729 } 2730 v1Fallback = true 2731 assertRequest(c, r, "GET", searchPath) 2732 } else { 2733 assertRequest(c, r, "GET", findPath) 2734 v2Hit = true 2735 } 2736 c.Check(r.URL.Query().Get("q"), Equals, "hello") 2737 http.Error(w, http.StatusText(418), 418) // I'm a teapot 2738 })) 2739 c.Assert(mockServer, NotNil) 2740 defer mockServer.Close() 2741 2742 mockServerURL, _ := url.Parse(mockServer.URL) 2743 cfg := store.Config{ 2744 StoreBaseURL: mockServerURL, 2745 DetailFields: []string{}, // make the error less noisy 2746 FindFields: []string{}, 2747 } 2748 sto := store.New(&cfg, nil) 2749 2750 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2751 c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 418 via GET to "http://\S+[?&]q=hello.*"`) 2752 c.Check(snaps, HasLen, 0) 2753 if apiV1 { 2754 c.Check(v1Fallback, Equals, true) 2755 } else { 2756 c.Check(v2Hit, Equals, true) 2757 } 2758 } 2759 2760 func (s *storeTestSuite) TestFindV1Fails(c *C) { 2761 apiV1 := true 2762 s.testFindFails(c, apiV1) 2763 } 2764 2765 func (s *storeTestSuite) TestFindV2Fails(c *C) { 2766 s.testFindFails(c, false) 2767 } 2768 2769 func (s *storeTestSuite) testFindBadContentType(c *C, apiV1 bool) { 2770 var v1Fallback, v2Hit bool 2771 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2772 if apiV1 { 2773 if strings.Contains(r.URL.Path, findPath) { 2774 forceSearchV1(w) 2775 return 2776 } 2777 v1Fallback = true 2778 assertRequest(c, r, "GET", searchPath) 2779 } else { 2780 v2Hit = true 2781 assertRequest(c, r, "GET", findPath) 2782 } 2783 c.Check(r.URL.Query().Get("q"), Equals, "hello") 2784 if apiV1 { 2785 io.WriteString(w, MockSearchJSON) 2786 } else { 2787 io.WriteString(w, MockSearchJSONv2) 2788 } 2789 })) 2790 c.Assert(mockServer, NotNil) 2791 defer mockServer.Close() 2792 2793 mockServerURL, _ := url.Parse(mockServer.URL) 2794 cfg := store.Config{ 2795 StoreBaseURL: mockServerURL, 2796 DetailFields: []string{}, // make the error less noisy 2797 FindFields: []string{}, 2798 } 2799 sto := store.New(&cfg, nil) 2800 2801 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2802 c.Check(err, ErrorMatches, `received an unexpected content type \("text/plain[^"]+"\) when trying to search via "http://\S+[?&]q=hello.*"`) 2803 c.Check(snaps, HasLen, 0) 2804 if apiV1 { 2805 c.Check(v1Fallback, Equals, true) 2806 } else { 2807 c.Check(v2Hit, Equals, true) 2808 } 2809 } 2810 2811 func (s *storeTestSuite) TestFindV1BadContentType(c *C) { 2812 apiV1 := true 2813 s.testFindBadContentType(c, apiV1) 2814 } 2815 2816 func (s *storeTestSuite) TestFindV2BadContentType(c *C) { 2817 s.testFindBadContentType(c, false) 2818 } 2819 2820 func (s *storeTestSuite) testFindBadBody(c *C, apiV1 bool) { 2821 var v1Fallback, v2Hit bool 2822 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2823 if apiV1 { 2824 if strings.Contains(r.URL.Path, findPath) { 2825 forceSearchV1(w) 2826 return 2827 } 2828 v1Fallback = true 2829 assertRequest(c, r, "GET", searchPath) 2830 } else { 2831 v2Hit = true 2832 assertRequest(c, r, "GET", findPath) 2833 } 2834 query := r.URL.Query() 2835 c.Check(query.Get("q"), Equals, "hello") 2836 if apiV1 { 2837 w.Header().Set("Content-Type", "application/hal+json") 2838 } else { 2839 w.Header().Set("Content-Type", "application/json") 2840 } 2841 io.WriteString(w, "<hello>") 2842 })) 2843 c.Assert(mockServer, NotNil) 2844 defer mockServer.Close() 2845 2846 mockServerURL, _ := url.Parse(mockServer.URL) 2847 cfg := store.Config{ 2848 StoreBaseURL: mockServerURL, 2849 DetailFields: []string{}, // make the error less noisy 2850 FindFields: []string{}, 2851 } 2852 sto := store.New(&cfg, nil) 2853 2854 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2855 c.Check(err, ErrorMatches, `invalid character '<' looking for beginning of value`) 2856 c.Check(snaps, HasLen, 0) 2857 if apiV1 { 2858 c.Check(v1Fallback, Equals, true) 2859 } else { 2860 c.Check(v2Hit, Equals, true) 2861 } 2862 } 2863 2864 func (s *storeTestSuite) TestFindV1BadBody(c *C) { 2865 apiV1 := true 2866 s.testFindBadBody(c, apiV1) 2867 } 2868 2869 func (s *storeTestSuite) TestFindV2BadBody(c *C) { 2870 s.testFindBadBody(c, false) 2871 } 2872 2873 func (s *storeTestSuite) TestFindV2_404NoFallbackIfNewStore(c *C) { 2874 n := 0 2875 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2876 c.Assert(n, Equals, 0) 2877 n++ 2878 assertRequest(c, r, "GET", findPath) 2879 c.Check(r.URL.Query().Get("q"), Equals, "hello") 2880 w.Header().Set("Snap-Store-Version", "30") 2881 w.WriteHeader(404) 2882 })) 2883 c.Assert(mockServer, NotNil) 2884 defer mockServer.Close() 2885 2886 mockServerURL, _ := url.Parse(mockServer.URL) 2887 cfg := store.Config{ 2888 StoreBaseURL: mockServerURL, 2889 FindFields: []string{}, 2890 } 2891 sto := store.New(&cfg, nil) 2892 2893 _, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2894 c.Check(err, ErrorMatches, `.*got unexpected HTTP status code 404.*`) 2895 c.Check(n, Equals, 1) 2896 } 2897 2898 // testFindPermanent500 checks that a permanent 500 error on every request 2899 // results in 5 retries, after which the caller gets the 500 status. 2900 func (s *storeTestSuite) testFindPermanent500(c *C, apiV1 bool) { 2901 var n = 0 2902 var v1Fallback, v2Hit bool 2903 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2904 if apiV1 { 2905 if strings.Contains(r.URL.Path, findPath) { 2906 forceSearchV1(w) 2907 return 2908 } 2909 v1Fallback = true 2910 assertRequest(c, r, "GET", searchPath) 2911 } else { 2912 v2Hit = true 2913 assertRequest(c, r, "GET", findPath) 2914 } 2915 n++ 2916 w.WriteHeader(500) 2917 })) 2918 c.Assert(mockServer, NotNil) 2919 defer mockServer.Close() 2920 2921 mockServerURL, _ := url.Parse(mockServer.URL) 2922 cfg := store.Config{ 2923 StoreBaseURL: mockServerURL, 2924 DetailFields: []string{}, 2925 FindFields: []string{}, 2926 } 2927 sto := store.New(&cfg, nil) 2928 2929 _, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2930 c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 500 via GET to "http://\S+[?&]q=hello.*"`) 2931 c.Assert(n, Equals, 5) 2932 if apiV1 { 2933 c.Check(v1Fallback, Equals, true) 2934 } else { 2935 c.Check(v2Hit, Equals, true) 2936 } 2937 } 2938 2939 func (s *storeTestSuite) TestFindV1Permanent500(c *C) { 2940 apiV1 := true 2941 s.testFindPermanent500(c, apiV1) 2942 } 2943 2944 func (s *storeTestSuite) TestFindV2Permanent500(c *C) { 2945 s.testFindPermanent500(c, false) 2946 } 2947 2948 // testFind500OnceThenSucceed checks that a single 500 failure, followed by 2949 // a successful response is handled. 2950 func (s *storeTestSuite) testFind500OnceThenSucceed(c *C, apiV1 bool) { 2951 var n = 0 2952 var v1Fallback, v2Hit bool 2953 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 2954 if apiV1 { 2955 if strings.Contains(r.URL.Path, findPath) { 2956 forceSearchV1(w) 2957 return 2958 } 2959 v1Fallback = true 2960 assertRequest(c, r, "GET", searchPath) 2961 } else { 2962 v2Hit = true 2963 assertRequest(c, r, "GET", findPath) 2964 } 2965 n++ 2966 if n == 1 { 2967 w.WriteHeader(500) 2968 } else { 2969 if apiV1 { 2970 w.Header().Set("Content-Type", "application/hal+json") 2971 w.WriteHeader(200) 2972 io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1)) 2973 } else { 2974 w.Header().Set("Content-Type", "application/json") 2975 w.WriteHeader(200) 2976 io.WriteString(w, strings.Replace(MockSearchJSONv2, `"EUR": "2.99", "USD": "3.49"`, "", -1)) 2977 } 2978 } 2979 })) 2980 c.Assert(mockServer, NotNil) 2981 defer mockServer.Close() 2982 2983 mockServerURL, _ := url.Parse(mockServer.URL) 2984 cfg := store.Config{ 2985 StoreBaseURL: mockServerURL, 2986 DetailFields: []string{}, 2987 FindFields: []string{}, 2988 } 2989 sto := store.New(&cfg, nil) 2990 2991 snaps, err := sto.Find(s.ctx, &store.Search{Query: "hello"}, nil) 2992 c.Check(err, IsNil) 2993 c.Assert(snaps, HasLen, 1) 2994 c.Assert(n, Equals, 2) 2995 if apiV1 { 2996 c.Check(v1Fallback, Equals, true) 2997 } else { 2998 c.Check(v2Hit, Equals, true) 2999 } 3000 } 3001 3002 func (s *storeTestSuite) TestFindV1_500Once(c *C) { 3003 apiV1 := true 3004 s.testFind500OnceThenSucceed(c, apiV1) 3005 } 3006 3007 func (s *storeTestSuite) TestFindV2_500Once(c *C) { 3008 s.testFind500OnceThenSucceed(c, false) 3009 } 3010 3011 func (s *storeTestSuite) testFindAuthFailed(c *C, apiV1 bool) { 3012 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3013 if apiV1 { 3014 if strings.Contains(r.URL.Path, findPath) { 3015 forceSearchV1(w) 3016 return 3017 } 3018 } 3019 switch r.URL.Path { 3020 case searchPath: 3021 c.Assert(apiV1, Equals, true) 3022 fallthrough 3023 case findPath: 3024 // check authorization is set 3025 authorization := r.Header.Get("Authorization") 3026 c.Check(authorization, Equals, expectedAuthorization(c, s.user)) 3027 3028 query := r.URL.Query() 3029 c.Check(query.Get("q"), Equals, "foo") 3030 if release.OnClassic { 3031 c.Check(query.Get("confinement"), Matches, `strict,classic|classic,strict`) 3032 } else { 3033 c.Check(query.Get("confinement"), Equals, "strict") 3034 } 3035 if apiV1 { 3036 w.Header().Set("Content-Type", "application/hal+json") 3037 io.WriteString(w, MockSearchJSON) 3038 } else { 3039 w.Header().Set("Content-Type", "application/json") 3040 io.WriteString(w, MockSearchJSONv2) 3041 } 3042 case ordersPath: 3043 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3044 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3045 c.Check(r.URL.Path, Equals, ordersPath) 3046 w.WriteHeader(401) 3047 io.WriteString(w, "{}") 3048 default: 3049 c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path) 3050 } 3051 })) 3052 c.Assert(mockServer, NotNil) 3053 defer mockServer.Close() 3054 3055 mockServerURL, _ := url.Parse(mockServer.URL) 3056 cfg := store.Config{ 3057 StoreBaseURL: mockServerURL, 3058 DetailFields: []string{}, // make the error less noisy 3059 } 3060 sto := store.New(&cfg, nil) 3061 3062 snaps, err := sto.Find(s.ctx, &store.Search{Query: "foo"}, s.user) 3063 c.Assert(err, IsNil) 3064 3065 // Check that we log an error. 3066 c.Check(s.logbuf.String(), Matches, "(?ms).* cannot get user orders: invalid credentials") 3067 3068 // But still successfully return snap information. 3069 c.Assert(snaps, HasLen, 1) 3070 c.Check(snaps[0].SnapID, Equals, helloWorldSnapID) 3071 c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49}) 3072 c.Check(snaps[0].MustBuy, Equals, true) 3073 } 3074 3075 func (s *storeTestSuite) TestFindV1AuthFailed(c *C) { 3076 apiV1 := true 3077 s.testFindAuthFailed(c, apiV1) 3078 } 3079 3080 func (s *storeTestSuite) TestFindV2AuthFailed(c *C) { 3081 s.testFindAuthFailed(c, false) 3082 } 3083 3084 func (s *storeTestSuite) testFindCommonIDs(c *C, apiV1 bool) { 3085 n := 0 3086 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3087 if apiV1 { 3088 if strings.Contains(r.URL.Path, findPath) { 3089 forceSearchV1(w) 3090 return 3091 } 3092 assertRequest(c, r, "GET", searchPath) 3093 } else { 3094 assertRequest(c, r, "GET", findPath) 3095 } 3096 query := r.URL.Query() 3097 3098 name := query.Get("name") 3099 q := query.Get("q") 3100 3101 switch n { 3102 case 0: 3103 if apiV1 { 3104 c.Check(r.URL.Path, Matches, ".*/search") 3105 } else { 3106 c.Check(r.URL.Path, Matches, ".*/find") 3107 } 3108 c.Check(name, Equals, "") 3109 c.Check(q, Equals, "foo") 3110 default: 3111 c.Fatalf("what? %d", n) 3112 } 3113 3114 if apiV1 { 3115 w.Header().Set("Content-Type", "application/hal+json") 3116 w.WriteHeader(200) 3117 io.WriteString(w, strings.Replace(MockSearchJSON, 3118 `"common_ids": []`, 3119 `"common_ids": ["org.hello"]`, -1)) 3120 } else { 3121 w.Header().Set("Content-Type", "application/json") 3122 w.WriteHeader(200) 3123 io.WriteString(w, MockSearchJSONv2) 3124 } 3125 3126 n++ 3127 })) 3128 c.Assert(mockServer, NotNil) 3129 defer mockServer.Close() 3130 3131 serverURL, _ := url.Parse(mockServer.URL) 3132 cfg := store.Config{ 3133 StoreBaseURL: serverURL, 3134 } 3135 sto := store.New(&cfg, nil) 3136 3137 infos, err := sto.Find(s.ctx, &store.Search{Query: "foo"}, nil) 3138 c.Check(err, IsNil) 3139 c.Assert(infos, HasLen, 1) 3140 if apiV1 { 3141 c.Check(infos[0].CommonIDs, DeepEquals, []string{"org.hello"}) 3142 } else { 3143 c.Check(infos[0].CommonIDs, DeepEquals, []string{"aaa", "bbb"}) 3144 } 3145 } 3146 3147 func (s *storeTestSuite) TestFindV1CommonIDs(c *C) { 3148 apiV1 := true 3149 s.testFindCommonIDs(c, apiV1) 3150 } 3151 3152 func (s *storeTestSuite) TestFindV2CommonIDs(c *C) { 3153 s.testFindCommonIDs(c, false) 3154 } 3155 3156 func (s *storeTestSuite) testFindByCommonID(c *C, apiV1 bool) { 3157 n := 0 3158 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3159 if apiV1 { 3160 if strings.Contains(r.URL.Path, findPath) { 3161 forceSearchV1(w) 3162 return 3163 } 3164 assertRequest(c, r, "GET", searchPath) 3165 } else { 3166 assertRequest(c, r, "GET", findPath) 3167 } 3168 query := r.URL.Query() 3169 3170 switch n { 3171 case 0: 3172 if apiV1 { 3173 c.Check(r.URL.Path, Matches, ".*/search") 3174 c.Check(query["common_id"], DeepEquals, []string{"org.hello"}) 3175 } else { 3176 c.Check(r.URL.Path, Matches, ".*/find") 3177 c.Check(query["common-id"], DeepEquals, []string{"org.hello"}) 3178 } 3179 c.Check(query["name"], IsNil) 3180 c.Check(query["q"], IsNil) 3181 default: 3182 c.Fatalf("expected 1 query, now on %d", n+1) 3183 } 3184 3185 if apiV1 { 3186 w.Header().Set("Content-Type", "application/hal+json") 3187 w.WriteHeader(200) 3188 io.WriteString(w, strings.Replace(MockSearchJSON, 3189 `"common_ids": []`, 3190 `"common_ids": ["org.hello"]`, -1)) 3191 } else { 3192 w.Header().Set("Content-Type", "application/json") 3193 w.WriteHeader(200) 3194 io.WriteString(w, MockSearchJSONv2) 3195 } 3196 3197 n++ 3198 })) 3199 c.Assert(mockServer, NotNil) 3200 defer mockServer.Close() 3201 3202 serverURL, _ := url.Parse(mockServer.URL) 3203 cfg := store.Config{ 3204 StoreBaseURL: serverURL, 3205 } 3206 sto := store.New(&cfg, nil) 3207 3208 infos, err := sto.Find(s.ctx, &store.Search{CommonID: "org.hello"}, nil) 3209 c.Check(err, IsNil) 3210 c.Assert(infos, HasLen, 1) 3211 if apiV1 { 3212 c.Check(infos[0].CommonIDs, DeepEquals, []string{"org.hello"}) 3213 } else { 3214 c.Check(infos[0].CommonIDs, DeepEquals, []string{"aaa", "bbb"}) 3215 } 3216 } 3217 3218 func (s *storeTestSuite) TestFindV1ByCommonID(c *C) { 3219 apiV1 := true 3220 s.testFindByCommonID(c, apiV1) 3221 } 3222 3223 func (s *storeTestSuite) TestFindV2ByCommonID(c *C) { 3224 s.testFindByCommonID(c, false) 3225 } 3226 3227 func (s *storeTestSuite) TestFindClientUserAgent(c *C) { 3228 clientUserAgent := "some-client/1.0" 3229 3230 serverWasHit := false 3231 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3232 c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, clientUserAgent) 3233 serverWasHit = true 3234 3235 http.Error(w, http.StatusText(418), 418) // I'm a teapot 3236 })) 3237 c.Assert(mockServer, NotNil) 3238 defer mockServer.Close() 3239 3240 mockServerURL, _ := url.Parse(mockServer.URL) 3241 cfg := store.Config{ 3242 StoreBaseURL: mockServerURL, 3243 DetailFields: []string{}, // make the error less noisy 3244 } 3245 3246 req, err := http.NewRequest("GET", "/", nil) 3247 c.Assert(err, IsNil) 3248 req.Header.Add("User-Agent", clientUserAgent) 3249 ctx := store.WithClientUserAgent(s.ctx, req) 3250 3251 sto := store.New(&cfg, nil) 3252 sto.Find(ctx, &store.Search{Query: "hello"}, nil) 3253 c.Assert(serverWasHit, Equals, true) 3254 } 3255 3256 func (s *storeTestSuite) TestAuthLocationDependsOnEnviron(c *C) { 3257 defer snapdenv.MockUseStagingStore(false)() 3258 before := store.AuthLocation() 3259 3260 snapdenv.MockUseStagingStore(true) 3261 after := store.AuthLocation() 3262 3263 c.Check(before, Not(Equals), after) 3264 } 3265 3266 func (s *storeTestSuite) TestAuthURLDependsOnEnviron(c *C) { 3267 defer snapdenv.MockUseStagingStore(false)() 3268 before := store.AuthURL() 3269 3270 snapdenv.MockUseStagingStore(true) 3271 after := store.AuthURL() 3272 3273 c.Check(before, Not(Equals), after) 3274 } 3275 3276 func (s *storeTestSuite) TestApiURLDependsOnEnviron(c *C) { 3277 defer snapdenv.MockUseStagingStore(false)() 3278 before := store.ApiURL() 3279 3280 snapdenv.MockUseStagingStore(true) 3281 after := store.ApiURL() 3282 3283 c.Check(before, Not(Equals), after) 3284 } 3285 3286 func (s *storeTestSuite) TestStoreURLDependsOnEnviron(c *C) { 3287 // This also depends on the API URL, but that's tested separately (see 3288 // TestApiURLDependsOnEnviron). 3289 api := store.ApiURL() 3290 3291 c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", ""), IsNil) 3292 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", ""), IsNil) 3293 3294 // Test in order of precedence (low first) leaving env vars set as we go ... 3295 3296 u, err := store.StoreURL(api) 3297 c.Assert(err, IsNil) 3298 c.Check(u.String(), Matches, api.String()+".*") 3299 3300 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "https://force-api.local/"), IsNil) 3301 defer os.Setenv("SNAPPY_FORCE_API_URL", "") 3302 u, err = store.StoreURL(api) 3303 c.Assert(err, IsNil) 3304 c.Check(u.String(), Matches, "https://force-api.local/.*") 3305 3306 c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", "https://force-cpi.local/api/v1/"), IsNil) 3307 defer os.Setenv("SNAPPY_FORCE_CPI_URL", "") 3308 u, err = store.StoreURL(api) 3309 c.Assert(err, IsNil) 3310 c.Check(u.String(), Matches, "https://force-cpi.local/.*") 3311 } 3312 3313 func (s *storeTestSuite) TestStoreURLBadEnvironAPI(c *C) { 3314 c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "://force-api.local/"), IsNil) 3315 defer os.Setenv("SNAPPY_FORCE_API_URL", "") 3316 _, err := store.StoreURL(store.ApiURL()) 3317 c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_API_URL: parse \"?://force-api.local/\"?: missing protocol scheme") 3318 } 3319 3320 func (s *storeTestSuite) TestStoreURLBadEnvironCPI(c *C) { 3321 c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", "://force-cpi.local/api/v1/"), IsNil) 3322 defer os.Setenv("SNAPPY_FORCE_CPI_URL", "") 3323 _, err := store.StoreURL(store.ApiURL()) 3324 c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_CPI_URL: parse \"?://force-cpi.local/\"?: missing protocol scheme") 3325 } 3326 3327 func (s *storeTestSuite) TestStoreDeveloperURLDependsOnEnviron(c *C) { 3328 defer snapdenv.MockUseStagingStore(false)() 3329 before := store.StoreDeveloperURL() 3330 3331 snapdenv.MockUseStagingStore(true) 3332 after := store.StoreDeveloperURL() 3333 3334 c.Check(before, Not(Equals), after) 3335 } 3336 3337 func (s *storeTestSuite) TestStoreDefaultConfig(c *C) { 3338 c.Check(store.DefaultConfig().StoreBaseURL.String(), Equals, "https://api.snapcraft.io/") 3339 c.Check(store.DefaultConfig().AssertionsBaseURL, IsNil) 3340 } 3341 3342 func (s *storeTestSuite) TestNew(c *C) { 3343 aStore := store.New(nil, nil) 3344 c.Assert(aStore, NotNil) 3345 // check for fields 3346 c.Check(aStore.DetailFields(), DeepEquals, store.DefaultConfig().DetailFields) 3347 } 3348 3349 func (s *storeTestSuite) TestSuggestedCurrency(c *C) { 3350 suggestedCurrency := "GBP" 3351 3352 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3353 assertRequest(c, r, "GET", infoPathPattern) 3354 w.Header().Set("X-Suggested-Currency", suggestedCurrency) 3355 w.WriteHeader(200) 3356 3357 io.WriteString(w, mockInfoJSON) 3358 })) 3359 3360 c.Assert(mockServer, NotNil) 3361 defer mockServer.Close() 3362 3363 mockServerURL, _ := url.Parse(mockServer.URL) 3364 cfg := store.Config{ 3365 StoreBaseURL: mockServerURL, 3366 } 3367 sto := store.New(&cfg, nil) 3368 3369 // the store doesn't know the currency until after the first search, so fall back to dollars 3370 c.Check(sto.SuggestedCurrency(), Equals, "USD") 3371 3372 // we should soon have a suggested currency 3373 spec := store.SnapSpec{ 3374 Name: "hello-world", 3375 } 3376 result, err := sto.SnapInfo(s.ctx, spec, nil) 3377 c.Assert(err, IsNil) 3378 c.Assert(result, NotNil) 3379 c.Check(sto.SuggestedCurrency(), Equals, "GBP") 3380 3381 suggestedCurrency = "EUR" 3382 3383 // checking the currency updates 3384 result, err = sto.SnapInfo(s.ctx, spec, nil) 3385 c.Assert(err, IsNil) 3386 c.Assert(result, NotNil) 3387 c.Check(sto.SuggestedCurrency(), Equals, "EUR") 3388 } 3389 3390 func (s *storeTestSuite) TestDecorateOrders(c *C) { 3391 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3392 assertRequest(c, r, "GET", ordersPath) 3393 // check device authorization is set, implicitly checking doRequest was used 3394 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3395 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3396 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3397 c.Check(r.URL.Path, Equals, ordersPath) 3398 io.WriteString(w, mockOrdersJSON) 3399 })) 3400 3401 c.Assert(mockPurchasesServer, NotNil) 3402 defer mockPurchasesServer.Close() 3403 3404 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3405 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3406 cfg := store.Config{ 3407 StoreBaseURL: mockServerURL, 3408 } 3409 sto := store.New(&cfg, dauthCtx) 3410 3411 helloWorld := &snap.Info{} 3412 helloWorld.SnapID = helloWorldSnapID 3413 helloWorld.Prices = map[string]float64{"USD": 1.23} 3414 helloWorld.Paid = true 3415 3416 funkyApp := &snap.Info{} 3417 funkyApp.SnapID = funkyAppSnapID 3418 funkyApp.Prices = map[string]float64{"USD": 2.34} 3419 funkyApp.Paid = true 3420 3421 otherApp := &snap.Info{} 3422 otherApp.SnapID = "other" 3423 otherApp.Prices = map[string]float64{"USD": 3.45} 3424 otherApp.Paid = true 3425 3426 otherApp2 := &snap.Info{} 3427 otherApp2.SnapID = "other2" 3428 3429 snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2} 3430 3431 err := sto.DecorateOrders(snaps, s.user) 3432 c.Assert(err, IsNil) 3433 3434 c.Check(helloWorld.MustBuy, Equals, false) 3435 c.Check(funkyApp.MustBuy, Equals, false) 3436 c.Check(otherApp.MustBuy, Equals, true) 3437 c.Check(otherApp2.MustBuy, Equals, false) 3438 } 3439 3440 func (s *storeTestSuite) TestDecorateOrdersFailedAccess(c *C) { 3441 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3442 assertRequest(c, r, "GET", ordersPath) 3443 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3444 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3445 c.Check(r.URL.Path, Equals, ordersPath) 3446 w.WriteHeader(401) 3447 io.WriteString(w, "{}") 3448 })) 3449 3450 c.Assert(mockPurchasesServer, NotNil) 3451 defer mockPurchasesServer.Close() 3452 3453 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3454 cfg := store.Config{ 3455 StoreBaseURL: mockServerURL, 3456 } 3457 sto := store.New(&cfg, nil) 3458 3459 helloWorld := &snap.Info{} 3460 helloWorld.SnapID = helloWorldSnapID 3461 helloWorld.Prices = map[string]float64{"USD": 1.23} 3462 helloWorld.Paid = true 3463 3464 funkyApp := &snap.Info{} 3465 funkyApp.SnapID = funkyAppSnapID 3466 funkyApp.Prices = map[string]float64{"USD": 2.34} 3467 funkyApp.Paid = true 3468 3469 otherApp := &snap.Info{} 3470 otherApp.SnapID = "other" 3471 otherApp.Prices = map[string]float64{"USD": 3.45} 3472 otherApp.Paid = true 3473 3474 otherApp2 := &snap.Info{} 3475 otherApp2.SnapID = "other2" 3476 3477 snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2} 3478 3479 err := sto.DecorateOrders(snaps, s.user) 3480 c.Assert(err, NotNil) 3481 3482 c.Check(helloWorld.MustBuy, Equals, true) 3483 c.Check(funkyApp.MustBuy, Equals, true) 3484 c.Check(otherApp.MustBuy, Equals, true) 3485 c.Check(otherApp2.MustBuy, Equals, false) 3486 } 3487 3488 func (s *storeTestSuite) TestDecorateOrdersNoAuth(c *C) { 3489 cfg := store.Config{} 3490 sto := store.New(&cfg, nil) 3491 3492 helloWorld := &snap.Info{} 3493 helloWorld.SnapID = helloWorldSnapID 3494 helloWorld.Prices = map[string]float64{"USD": 1.23} 3495 helloWorld.Paid = true 3496 3497 funkyApp := &snap.Info{} 3498 funkyApp.SnapID = funkyAppSnapID 3499 funkyApp.Prices = map[string]float64{"USD": 2.34} 3500 funkyApp.Paid = true 3501 3502 otherApp := &snap.Info{} 3503 otherApp.SnapID = "other" 3504 otherApp.Prices = map[string]float64{"USD": 3.45} 3505 otherApp.Paid = true 3506 3507 otherApp2 := &snap.Info{} 3508 otherApp2.SnapID = "other2" 3509 3510 snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2} 3511 3512 err := sto.DecorateOrders(snaps, nil) 3513 c.Assert(err, IsNil) 3514 3515 c.Check(helloWorld.MustBuy, Equals, true) 3516 c.Check(funkyApp.MustBuy, Equals, true) 3517 c.Check(otherApp.MustBuy, Equals, true) 3518 c.Check(otherApp2.MustBuy, Equals, false) 3519 } 3520 3521 func (s *storeTestSuite) TestDecorateOrdersAllFree(c *C) { 3522 requestRecieved := false 3523 3524 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3525 c.Error(r.URL.Path) 3526 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3527 requestRecieved = true 3528 io.WriteString(w, `{"orders": []}`) 3529 })) 3530 3531 c.Assert(mockPurchasesServer, NotNil) 3532 defer mockPurchasesServer.Close() 3533 3534 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3535 cfg := store.Config{ 3536 StoreBaseURL: mockServerURL, 3537 } 3538 3539 sto := store.New(&cfg, nil) 3540 3541 // This snap is free 3542 helloWorld := &snap.Info{} 3543 helloWorld.SnapID = helloWorldSnapID 3544 3545 // This snap is also free 3546 funkyApp := &snap.Info{} 3547 funkyApp.SnapID = funkyAppSnapID 3548 3549 snaps := []*snap.Info{helloWorld, funkyApp} 3550 3551 // There should be no request to the purchase server. 3552 err := sto.DecorateOrders(snaps, s.user) 3553 c.Assert(err, IsNil) 3554 c.Check(requestRecieved, Equals, false) 3555 } 3556 3557 func (s *storeTestSuite) TestDecorateOrdersSingle(c *C) { 3558 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3559 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3560 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3561 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3562 c.Check(r.URL.Path, Equals, ordersPath) 3563 io.WriteString(w, mockSingleOrderJSON) 3564 })) 3565 3566 c.Assert(mockPurchasesServer, NotNil) 3567 defer mockPurchasesServer.Close() 3568 3569 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3570 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3571 cfg := store.Config{ 3572 StoreBaseURL: mockServerURL, 3573 } 3574 sto := store.New(&cfg, dauthCtx) 3575 3576 helloWorld := &snap.Info{} 3577 helloWorld.SnapID = helloWorldSnapID 3578 helloWorld.Prices = map[string]float64{"USD": 1.23} 3579 helloWorld.Paid = true 3580 3581 snaps := []*snap.Info{helloWorld} 3582 3583 err := sto.DecorateOrders(snaps, s.user) 3584 c.Assert(err, IsNil) 3585 c.Check(helloWorld.MustBuy, Equals, false) 3586 } 3587 3588 func (s *storeTestSuite) TestDecorateOrdersSingleFreeSnap(c *C) { 3589 cfg := store.Config{} 3590 sto := store.New(&cfg, nil) 3591 3592 helloWorld := &snap.Info{} 3593 helloWorld.SnapID = helloWorldSnapID 3594 3595 snaps := []*snap.Info{helloWorld} 3596 3597 err := sto.DecorateOrders(snaps, s.user) 3598 c.Assert(err, IsNil) 3599 c.Check(helloWorld.MustBuy, Equals, false) 3600 } 3601 3602 func (s *storeTestSuite) TestDecorateOrdersSingleNotFound(c *C) { 3603 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3604 assertRequest(c, r, "GET", ordersPath) 3605 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3606 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3607 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3608 c.Check(r.URL.Path, Equals, ordersPath) 3609 w.WriteHeader(404) 3610 io.WriteString(w, "{}") 3611 })) 3612 3613 c.Assert(mockPurchasesServer, NotNil) 3614 defer mockPurchasesServer.Close() 3615 3616 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3617 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3618 cfg := store.Config{ 3619 StoreBaseURL: mockServerURL, 3620 } 3621 sto := store.New(&cfg, dauthCtx) 3622 3623 helloWorld := &snap.Info{} 3624 helloWorld.SnapID = helloWorldSnapID 3625 helloWorld.Prices = map[string]float64{"USD": 1.23} 3626 helloWorld.Paid = true 3627 3628 snaps := []*snap.Info{helloWorld} 3629 3630 err := sto.DecorateOrders(snaps, s.user) 3631 c.Assert(err, NotNil) 3632 c.Check(helloWorld.MustBuy, Equals, true) 3633 } 3634 3635 func (s *storeTestSuite) TestDecorateOrdersTokenExpired(c *C) { 3636 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3637 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3638 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3639 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3640 c.Check(r.URL.Path, Equals, ordersPath) 3641 w.WriteHeader(401) 3642 io.WriteString(w, "") 3643 })) 3644 3645 c.Assert(mockPurchasesServer, NotNil) 3646 defer mockPurchasesServer.Close() 3647 3648 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 3649 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3650 cfg := store.Config{ 3651 StoreBaseURL: mockServerURL, 3652 } 3653 sto := store.New(&cfg, dauthCtx) 3654 3655 helloWorld := &snap.Info{} 3656 helloWorld.SnapID = helloWorldSnapID 3657 helloWorld.Prices = map[string]float64{"USD": 1.23} 3658 helloWorld.Paid = true 3659 3660 snaps := []*snap.Info{helloWorld} 3661 3662 err := sto.DecorateOrders(snaps, s.user) 3663 c.Assert(err, NotNil) 3664 c.Check(helloWorld.MustBuy, Equals, true) 3665 } 3666 3667 func (s *storeTestSuite) TestMustBuy(c *C) { 3668 // Never need to buy a free snap. 3669 c.Check(store.MustBuy(false, true), Equals, false) 3670 c.Check(store.MustBuy(false, false), Equals, false) 3671 3672 // Don't need to buy snaps that have been bought. 3673 c.Check(store.MustBuy(true, true), Equals, false) 3674 3675 // Need to buy snaps that aren't bought. 3676 c.Check(store.MustBuy(true, false), Equals, true) 3677 } 3678 3679 var buyTests = []struct { 3680 suggestedCurrency string 3681 expectedInput string 3682 buyStatus int 3683 buyResponse string 3684 buyErrorMessage string 3685 buyErrorCode string 3686 snapID string 3687 price float64 3688 currency string 3689 expectedResult *client.BuyResult 3690 expectedError string 3691 }{ 3692 { 3693 // successful buying 3694 suggestedCurrency: "EUR", 3695 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"0.99","currency":"EUR"}`, 3696 buyResponse: mockOrderResponseJSON, 3697 expectedResult: &client.BuyResult{State: "Complete"}, 3698 }, 3699 { 3700 // failure due to invalid price 3701 suggestedCurrency: "USD", 3702 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"5.99","currency":"USD"}`, 3703 buyStatus: 400, 3704 buyErrorCode: "invalid-field", 3705 buyErrorMessage: "invalid price specified", 3706 price: 5.99, 3707 expectedError: "cannot buy snap: bad request: invalid price specified", 3708 }, 3709 { 3710 // failure due to unknown snap ID 3711 suggestedCurrency: "USD", 3712 expectedInput: `{"snap_id":"invalid snap ID","amount":"0.99","currency":"EUR"}`, 3713 buyStatus: 404, 3714 buyErrorCode: "not-found", 3715 buyErrorMessage: "Snap package not found", 3716 snapID: "invalid snap ID", 3717 price: 0.99, 3718 currency: "EUR", 3719 expectedError: "cannot buy snap: server says not found: Snap package not found", 3720 }, 3721 { 3722 // failure due to "Purchase failed" 3723 suggestedCurrency: "USD", 3724 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`, 3725 buyStatus: 402, // Payment Required 3726 buyErrorCode: "request-failed", 3727 buyErrorMessage: "Purchase failed", 3728 expectedError: "payment declined", 3729 }, 3730 { 3731 // failure due to no payment methods 3732 suggestedCurrency: "USD", 3733 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`, 3734 buyStatus: 403, 3735 buyErrorCode: "no-payment-methods", 3736 buyErrorMessage: "No payment methods associated with your account.", 3737 expectedError: "no payment methods", 3738 }, 3739 { 3740 // failure due to terms of service not accepted 3741 suggestedCurrency: "USD", 3742 expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`, 3743 buyStatus: 403, 3744 buyErrorCode: "tos-not-accepted", 3745 buyErrorMessage: "You must accept the latest terms of service first.", 3746 expectedError: "terms of service not accepted", 3747 }, 3748 } 3749 3750 func (s *storeTestSuite) TestBuy500(c *C) { 3751 n := 0 3752 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3753 switch r.URL.Path { 3754 case detailsPath("hello-world"): 3755 n++ 3756 w.WriteHeader(500) 3757 case buyPath: 3758 case customersMePath: 3759 // default 200 response 3760 default: 3761 c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path) 3762 } 3763 })) 3764 c.Assert(mockServer, NotNil) 3765 defer mockServer.Close() 3766 3767 mockServerURL, _ := url.Parse(mockServer.URL) 3768 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3769 cfg := store.Config{ 3770 StoreBaseURL: mockServerURL, 3771 } 3772 sto := store.New(&cfg, dauthCtx) 3773 3774 buyOptions := &client.BuyOptions{ 3775 SnapID: helloWorldSnapID, 3776 Currency: "USD", 3777 Price: 1, 3778 } 3779 _, err := sto.Buy(buyOptions, s.user) 3780 c.Assert(err, NotNil) 3781 } 3782 3783 func (s *storeTestSuite) TestBuy(c *C) { 3784 for _, test := range buyTests { 3785 searchServerCalled := false 3786 purchaseServerGetCalled := false 3787 purchaseServerPostCalled := false 3788 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 3789 switch r.URL.Path { 3790 case infoPath("hello-world"): 3791 c.Assert(r.Method, Equals, "GET") 3792 w.Header().Set("Content-Type", "application/json") 3793 w.Header().Set("X-Suggested-Currency", test.suggestedCurrency) 3794 w.WriteHeader(200) 3795 io.WriteString(w, mockInfoJSON) 3796 searchServerCalled = true 3797 case ordersPath: 3798 c.Assert(r.Method, Equals, "GET") 3799 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3800 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3801 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3802 io.WriteString(w, `{"orders": []}`) 3803 purchaseServerGetCalled = true 3804 case buyPath: 3805 c.Assert(r.Method, Equals, "POST") 3806 // check device authorization is set, implicitly checking doRequest was used 3807 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 3808 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 3809 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 3810 c.Check(r.Header.Get("Content-Type"), Equals, store.JsonContentType) 3811 c.Check(r.URL.Path, Equals, buyPath) 3812 jsonReq, err := ioutil.ReadAll(r.Body) 3813 c.Assert(err, IsNil) 3814 c.Check(string(jsonReq), Equals, test.expectedInput) 3815 if test.buyErrorCode == "" { 3816 io.WriteString(w, test.buyResponse) 3817 } else { 3818 w.WriteHeader(test.buyStatus) 3819 // TODO(matt): this is fugly! 3820 fmt.Fprintf(w, ` 3821 { 3822 "error_list": [ 3823 { 3824 "code": "%s", 3825 "message": "%s" 3826 } 3827 ] 3828 }`, test.buyErrorCode, test.buyErrorMessage) 3829 } 3830 3831 purchaseServerPostCalled = true 3832 default: 3833 c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path) 3834 } 3835 })) 3836 c.Assert(mockServer, NotNil) 3837 defer mockServer.Close() 3838 3839 mockServerURL, _ := url.Parse(mockServer.URL) 3840 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 3841 cfg := store.Config{ 3842 StoreBaseURL: mockServerURL, 3843 } 3844 sto := store.New(&cfg, dauthCtx) 3845 3846 // Find the snap first 3847 spec := store.SnapSpec{ 3848 Name: "hello-world", 3849 } 3850 snap, err := sto.SnapInfo(s.ctx, spec, s.user) 3851 c.Assert(snap, NotNil) 3852 c.Assert(err, IsNil) 3853 3854 buyOptions := &client.BuyOptions{ 3855 SnapID: snap.SnapID, 3856 Currency: sto.SuggestedCurrency(), 3857 Price: snap.Prices[sto.SuggestedCurrency()], 3858 } 3859 if test.snapID != "" { 3860 buyOptions.SnapID = test.snapID 3861 } 3862 if test.currency != "" { 3863 buyOptions.Currency = test.currency 3864 } 3865 if test.price > 0 { 3866 buyOptions.Price = test.price 3867 } 3868 result, err := sto.Buy(buyOptions, s.user) 3869 3870 c.Check(result, DeepEquals, test.expectedResult) 3871 if test.expectedError == "" { 3872 c.Check(err, IsNil) 3873 } else { 3874 c.Assert(err, NotNil) 3875 c.Check(err.Error(), Equals, test.expectedError) 3876 } 3877 3878 c.Check(searchServerCalled, Equals, true) 3879 c.Check(purchaseServerGetCalled, Equals, true) 3880 c.Check(purchaseServerPostCalled, Equals, true) 3881 } 3882 } 3883 3884 func (s *storeTestSuite) TestBuyFailArgumentChecking(c *C) { 3885 sto := store.New(&store.Config{}, nil) 3886 3887 // no snap ID 3888 result, err := sto.Buy(&client.BuyOptions{ 3889 Price: 1.0, 3890 Currency: "USD", 3891 }, s.user) 3892 c.Assert(result, IsNil) 3893 c.Assert(err, NotNil) 3894 c.Check(err.Error(), Equals, "cannot buy snap: snap ID missing") 3895 3896 // no price 3897 result, err = sto.Buy(&client.BuyOptions{ 3898 SnapID: "snap ID", 3899 Currency: "USD", 3900 }, s.user) 3901 c.Assert(result, IsNil) 3902 c.Assert(err, NotNil) 3903 c.Check(err.Error(), Equals, "cannot buy snap: invalid expected price") 3904 3905 // no currency 3906 result, err = sto.Buy(&client.BuyOptions{ 3907 SnapID: "snap ID", 3908 Price: 1.0, 3909 }, s.user) 3910 c.Assert(result, IsNil) 3911 c.Assert(err, NotNil) 3912 c.Check(err.Error(), Equals, "cannot buy snap: currency missing") 3913 3914 // no user 3915 result, err = sto.Buy(&client.BuyOptions{ 3916 SnapID: "snap ID", 3917 Price: 1.0, 3918 Currency: "USD", 3919 }, nil) 3920 c.Assert(result, IsNil) 3921 c.Assert(err, NotNil) 3922 c.Check(err.Error(), Equals, "you need to log in first") 3923 } 3924 3925 var readyToBuyTests = []struct { 3926 Input func(w http.ResponseWriter) 3927 Test func(c *C, err error) 3928 NumOfCalls int 3929 }{ 3930 { 3931 // A user account the is ready for buying 3932 Input: func(w http.ResponseWriter) { 3933 io.WriteString(w, ` 3934 { 3935 "latest_tos_date": "2016-09-14T00:00:00+00:00", 3936 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3937 "latest_tos_accepted": true, 3938 "has_payment_method": true 3939 } 3940 `) 3941 }, 3942 Test: func(c *C, err error) { 3943 c.Check(err, IsNil) 3944 }, 3945 NumOfCalls: 1, 3946 }, 3947 { 3948 // A user account that hasn't accepted the TOS 3949 Input: func(w http.ResponseWriter) { 3950 io.WriteString(w, ` 3951 { 3952 "latest_tos_date": "2016-10-14T00:00:00+00:00", 3953 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3954 "latest_tos_accepted": false, 3955 "has_payment_method": true 3956 } 3957 `) 3958 }, 3959 Test: func(c *C, err error) { 3960 c.Assert(err, NotNil) 3961 c.Check(err.Error(), Equals, "terms of service not accepted") 3962 }, 3963 NumOfCalls: 1, 3964 }, 3965 { 3966 // A user account that has no payment method 3967 Input: func(w http.ResponseWriter) { 3968 io.WriteString(w, ` 3969 { 3970 "latest_tos_date": "2016-10-14T00:00:00+00:00", 3971 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3972 "latest_tos_accepted": true, 3973 "has_payment_method": false 3974 } 3975 `) 3976 }, 3977 Test: func(c *C, err error) { 3978 c.Assert(err, NotNil) 3979 c.Check(err.Error(), Equals, "no payment methods") 3980 }, 3981 NumOfCalls: 1, 3982 }, 3983 { 3984 // A user account that has no payment method and has not accepted the TOS 3985 Input: func(w http.ResponseWriter) { 3986 io.WriteString(w, ` 3987 { 3988 "latest_tos_date": "2016-10-14T00:00:00+00:00", 3989 "accepted_tos_date": "2016-09-14T15:56:49+00:00", 3990 "latest_tos_accepted": false, 3991 "has_payment_method": false 3992 } 3993 `) 3994 }, 3995 Test: func(c *C, err error) { 3996 c.Assert(err, NotNil) 3997 c.Check(err.Error(), Equals, "no payment methods") 3998 }, 3999 NumOfCalls: 1, 4000 }, 4001 { 4002 // No user account exists 4003 Input: func(w http.ResponseWriter) { 4004 w.WriteHeader(404) 4005 io.WriteString(w, "{}") 4006 }, 4007 Test: func(c *C, err error) { 4008 c.Assert(err, NotNil) 4009 c.Check(err.Error(), Equals, "cannot get customer details: server says no account exists") 4010 }, 4011 NumOfCalls: 1, 4012 }, 4013 { 4014 // An unknown set of errors occurs 4015 Input: func(w http.ResponseWriter) { 4016 w.WriteHeader(500) 4017 io.WriteString(w, ` 4018 { 4019 "error_list": [ 4020 { 4021 "code": "code 1", 4022 "message": "message 1" 4023 }, 4024 { 4025 "code": "code 2", 4026 "message": "message 2" 4027 } 4028 ] 4029 }`) 4030 }, 4031 Test: func(c *C, err error) { 4032 c.Assert(err, NotNil) 4033 c.Check(err.Error(), Equals, `message 1`) 4034 }, 4035 NumOfCalls: 5, 4036 }, 4037 } 4038 4039 func (s *storeTestSuite) TestReadyToBuy(c *C) { 4040 for _, test := range readyToBuyTests { 4041 purchaseServerGetCalled := 0 4042 mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4043 assertRequest(c, r, "GET", customersMePath) 4044 switch r.Method { 4045 case "GET": 4046 // check device authorization is set, implicitly checking doRequest was used 4047 c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 4048 c.Check(r.Header.Get("Authorization"), Equals, expectedAuthorization(c, s.user)) 4049 c.Check(r.Header.Get("Accept"), Equals, store.JsonContentType) 4050 c.Check(r.URL.Path, Equals, customersMePath) 4051 test.Input(w) 4052 purchaseServerGetCalled++ 4053 default: 4054 c.Error("Unexpected request method: ", r.Method) 4055 } 4056 })) 4057 4058 c.Assert(mockPurchasesServer, NotNil) 4059 defer mockPurchasesServer.Close() 4060 4061 mockServerURL, _ := url.Parse(mockPurchasesServer.URL) 4062 dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user} 4063 cfg := store.Config{ 4064 StoreBaseURL: mockServerURL, 4065 } 4066 sto := store.New(&cfg, dauthCtx) 4067 4068 err := sto.ReadyToBuy(s.user) 4069 test.Test(c, err) 4070 c.Check(purchaseServerGetCalled, Equals, test.NumOfCalls) 4071 } 4072 } 4073 4074 func (s *storeTestSuite) TestDoRequestSetRangeHeaderOnRedirect(c *C) { 4075 n := 0 4076 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4077 switch n { 4078 case 0: 4079 http.Redirect(w, r, r.URL.Path+"-else", 302) 4080 n++ 4081 case 1: 4082 c.Check(r.URL.Path, Equals, "/somewhere-else") 4083 rg := r.Header.Get("Range") 4084 c.Check(rg, Equals, "bytes=5-") 4085 default: 4086 panic("got more than 2 requests in this test") 4087 } 4088 })) 4089 4090 c.Assert(mockServer, NotNil) 4091 defer mockServer.Close() 4092 4093 url, err := url.Parse(mockServer.URL + "/somewhere") 4094 c.Assert(err, IsNil) 4095 reqOptions := store.NewRequestOptions("GET", url) 4096 reqOptions.ExtraHeaders = map[string]string{ 4097 "Range": "bytes=5-", 4098 } 4099 4100 sto := store.New(&store.Config{}, nil) 4101 _, err = sto.DoRequest(s.ctx, sto.Client(), reqOptions, s.user) 4102 c.Assert(err, IsNil) 4103 } 4104 4105 func (s *storeTestSuite) TestConnectivityCheckHappy(c *C) { 4106 seenPaths := make(map[string]int, 2) 4107 var mockServerURL *url.URL 4108 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4109 switch r.URL.Path { 4110 case "/v2/snaps/info/core": 4111 c.Check(r.Method, Equals, "GET") 4112 c.Check(r.URL.Query(), DeepEquals, url.Values{"fields": {"download"}, "architecture": {arch.DpkgArchitecture()}}) 4113 u, err := url.Parse("/download/core") 4114 c.Assert(err, IsNil) 4115 io.WriteString(w, 4116 fmt.Sprintf(`{"channel-map": [{"download": {"url": %q}}, {"download": {"url": %q}}, {"download": {"url": %q}}]}`, 4117 mockServerURL.ResolveReference(u).String(), 4118 mockServerURL.String()+"/bogus1/", 4119 mockServerURL.String()+"/bogus2/", 4120 )) 4121 case "/download/core": 4122 c.Check(r.Method, Equals, "HEAD") 4123 w.WriteHeader(200) 4124 default: 4125 c.Fatalf("unexpected request: %s", r.URL.String()) 4126 return 4127 } 4128 seenPaths[r.URL.Path]++ 4129 })) 4130 c.Assert(mockServer, NotNil) 4131 defer mockServer.Close() 4132 mockServerURL, _ = url.Parse(mockServer.URL) 4133 4134 sto := store.New(&store.Config{ 4135 StoreBaseURL: mockServerURL, 4136 }, nil) 4137 connectivity, err := sto.ConnectivityCheck() 4138 c.Assert(err, IsNil) 4139 // everything is the test server, here 4140 c.Check(connectivity, DeepEquals, map[string]bool{ 4141 mockServerURL.Host: true, 4142 }) 4143 c.Check(seenPaths, DeepEquals, map[string]int{ 4144 "/v2/snaps/info/core": 1, 4145 "/download/core": 1, 4146 }) 4147 } 4148 4149 func (s *storeTestSuite) TestConnectivityCheckUnhappy(c *C) { 4150 store.MockConnCheckStrategy(&s.BaseTest, retry.LimitCount(3, retry.Exponential{ 4151 Initial: time.Millisecond, 4152 Factor: 1.3, 4153 })) 4154 4155 seenPaths := make(map[string]int, 2) 4156 var mockServerURL *url.URL 4157 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4158 switch r.URL.Path { 4159 case "/v2/snaps/info/core": 4160 w.WriteHeader(500) 4161 default: 4162 c.Fatalf("unexpected request: %s", r.URL.String()) 4163 return 4164 } 4165 seenPaths[r.URL.Path]++ 4166 })) 4167 c.Assert(mockServer, NotNil) 4168 defer mockServer.Close() 4169 mockServerURL, _ = url.Parse(mockServer.URL) 4170 4171 sto := store.New(&store.Config{ 4172 StoreBaseURL: mockServerURL, 4173 }, nil) 4174 connectivity, err := sto.ConnectivityCheck() 4175 c.Assert(err, IsNil) 4176 // everything is the test server, here 4177 c.Check(connectivity, DeepEquals, map[string]bool{ 4178 mockServerURL.Host: false, 4179 }) 4180 // three because retries 4181 c.Check(seenPaths, DeepEquals, map[string]int{ 4182 "/v2/snaps/info/core": 3, 4183 }) 4184 } 4185 4186 func (s *storeTestSuite) TestCreateCohort(c *C) { 4187 mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 4188 assertRequest(c, r, "POST", cohortsPath) 4189 // check device authorization is set, implicitly checking doRequest was used 4190 c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`) 4191 4192 dec := json.NewDecoder(r.Body) 4193 var req struct { 4194 Snaps []string 4195 } 4196 err := dec.Decode(&req) 4197 c.Assert(err, IsNil) 4198 c.Check(dec.More(), Equals, false) 4199 4200 c.Check(req.Snaps, DeepEquals, []string{"foo", "bar"}) 4201 4202 io.WriteString(w, `{ 4203 "cohort-keys": { 4204 "potato": "U3VwZXIgc2VjcmV0IHN0dWZmIGVuY3J5cHRlZCBoZXJlLg==" 4205 } 4206 }`) 4207 })) 4208 4209 c.Assert(mockServer, NotNil) 4210 defer mockServer.Close() 4211 4212 mockServerURL, _ := url.Parse(mockServer.URL) 4213 cfg := store.Config{ 4214 StoreBaseURL: mockServerURL, 4215 } 4216 dauthCtx := &testDauthContext{c: c, device: s.device} 4217 sto := store.New(&cfg, dauthCtx) 4218 4219 cohorts, err := sto.CreateCohorts(s.ctx, []string{"foo", "bar"}) 4220 c.Assert(err, IsNil) 4221 c.Assert(cohorts, DeepEquals, map[string]string{ 4222 "potato": "U3VwZXIgc2VjcmV0IHN0dWZmIGVuY3J5cHRlZCBoZXJlLg==", 4223 }) 4224 }