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