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