github.com/david-imola/snapd@v0.0.0-20210611180407-2de8ddeece6d/store/store_test.go (about)

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