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  }