github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/store/store_test.go (about)

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