github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/daemon/api_find_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package daemon_test
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"net"
    26  	"net/http"
    27  	"net/url"
    28  
    29  	"gopkg.in/check.v1"
    30  
    31  	"github.com/snapcore/snapd/client"
    32  	"github.com/snapcore/snapd/httputil"
    33  	"github.com/snapcore/snapd/overlord/auth"
    34  	"github.com/snapcore/snapd/overlord/snapstate"
    35  	"github.com/snapcore/snapd/snap"
    36  	"github.com/snapcore/snapd/store"
    37  )
    38  
    39  var _ = check.Suite(&findSuite{})
    40  
    41  type findSuite struct {
    42  	apiBaseSuite
    43  }
    44  
    45  func (s *findSuite) TestFind(c *check.C) {
    46  	s.daemon(c)
    47  
    48  	s.suggestedCurrency = "EUR"
    49  
    50  	s.rsnaps = []*snap.Info{{
    51  		SideInfo: snap.SideInfo{
    52  			RealName: "store",
    53  		},
    54  		Publisher: snap.StoreAccount{
    55  			ID:          "foo-id",
    56  			Username:    "foo",
    57  			DisplayName: "Foo",
    58  			Validation:  "unproven",
    59  		},
    60  	}}
    61  
    62  	req, err := http.NewRequest("GET", "/v2/find?q=hi", nil)
    63  	c.Assert(err, check.IsNil)
    64  
    65  	rsp := s.syncReq(c, req, nil)
    66  
    67  	snaps := snapList(rsp.Result)
    68  	c.Assert(snaps, check.HasLen, 1)
    69  	c.Assert(snaps[0]["name"], check.Equals, "store")
    70  	c.Check(snaps[0]["prices"], check.IsNil)
    71  	c.Check(snaps[0]["channels"], check.IsNil)
    72  
    73  	c.Check(rsp.SuggestedCurrency, check.Equals, "EUR")
    74  
    75  	c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"})
    76  	c.Check(s.currentSnaps, check.HasLen, 0)
    77  	c.Check(s.actions, check.HasLen, 0)
    78  }
    79  
    80  func (s *findSuite) TestFindRefreshes(c *check.C) {
    81  	s.daemon(c)
    82  
    83  	s.rsnaps = []*snap.Info{{
    84  		SideInfo: snap.SideInfo{
    85  			RealName: "store",
    86  		},
    87  		Publisher: snap.StoreAccount{
    88  			ID:          "foo-id",
    89  			Username:    "foo",
    90  			DisplayName: "Foo",
    91  			Validation:  "unproven",
    92  		},
    93  	}}
    94  	s.mockSnap(c, "name: store\nversion: 1.0")
    95  
    96  	req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil)
    97  	c.Assert(err, check.IsNil)
    98  
    99  	rsp := s.syncReq(c, req, nil)
   100  
   101  	snaps := snapList(rsp.Result)
   102  	c.Assert(snaps, check.HasLen, 1)
   103  	c.Assert(snaps[0]["name"], check.Equals, "store")
   104  	c.Check(s.currentSnaps, check.HasLen, 1)
   105  	c.Check(s.actions, check.HasLen, 1)
   106  }
   107  
   108  func (s *findSuite) TestFindRefreshSideloaded(c *check.C) {
   109  	d := s.daemon(c)
   110  
   111  	s.rsnaps = []*snap.Info{{
   112  		SideInfo: snap.SideInfo{
   113  			RealName: "store",
   114  		},
   115  		Publisher: snap.StoreAccount{
   116  			ID:          "foo-id",
   117  			Username:    "foo",
   118  			DisplayName: "Foo",
   119  			Validation:  "unproven",
   120  		},
   121  	}}
   122  
   123  	s.mockSnap(c, "name: store\nversion: 1.0")
   124  
   125  	var snapst snapstate.SnapState
   126  	st := d.Overlord().State()
   127  	st.Lock()
   128  	err := snapstate.Get(st, "store", &snapst)
   129  	st.Unlock()
   130  	c.Assert(err, check.IsNil)
   131  	c.Assert(snapst.Sequence, check.HasLen, 1)
   132  
   133  	// clear the snapid
   134  	snapst.Sequence[0].SnapID = ""
   135  	st.Lock()
   136  	snapstate.Set(st, "store", &snapst)
   137  	st.Unlock()
   138  
   139  	req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil)
   140  	c.Assert(err, check.IsNil)
   141  
   142  	rsp := s.syncReq(c, req, nil)
   143  
   144  	snaps := snapList(rsp.Result)
   145  	c.Assert(snaps, check.HasLen, 0)
   146  	c.Check(s.currentSnaps, check.HasLen, 0)
   147  	c.Check(s.actions, check.HasLen, 0)
   148  }
   149  
   150  func (s *findSuite) TestFindPrivate(c *check.C) {
   151  	s.daemon(c)
   152  
   153  	s.rsnaps = []*snap.Info{}
   154  
   155  	req, err := http.NewRequest("GET", "/v2/find?q=foo&select=private", nil)
   156  	c.Assert(err, check.IsNil)
   157  
   158  	_ = s.syncReq(c, req, nil)
   159  
   160  	c.Check(s.storeSearch, check.DeepEquals, store.Search{
   161  		Query:   "foo",
   162  		Private: true,
   163  	})
   164  }
   165  
   166  func (s *findSuite) TestFindUserAgentContextCreated(c *check.C) {
   167  	s.daemon(c)
   168  
   169  	req, err := http.NewRequest("GET", "/v2/find", nil)
   170  	c.Assert(err, check.IsNil)
   171  	req.Header.Add("User-Agent", "some-agent/1.0")
   172  
   173  	_ = s.syncReq(c, req, nil)
   174  
   175  	c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0")
   176  }
   177  
   178  func (s *findSuite) TestFindOneUserAgentContextCreated(c *check.C) {
   179  	s.daemon(c)
   180  
   181  	s.rsnaps = []*snap.Info{{
   182  		SnapType: snap.TypeApp,
   183  		Version:  "v2",
   184  		SideInfo: snap.SideInfo{
   185  			RealName: "banana",
   186  		},
   187  		Publisher: snap.StoreAccount{
   188  			ID:          "foo-id",
   189  			Username:    "foo",
   190  			DisplayName: "Foo",
   191  			Validation:  "unproven",
   192  		},
   193  	}}
   194  	req, err := http.NewRequest("GET", "/v2/find?name=foo", nil)
   195  	c.Assert(err, check.IsNil)
   196  	req.Header.Add("User-Agent", "some-agent/1.0")
   197  
   198  	_ = s.syncReq(c, req, nil)
   199  
   200  	c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0")
   201  }
   202  
   203  func (s *findSuite) TestFindPrefix(c *check.C) {
   204  	s.daemon(c)
   205  
   206  	s.rsnaps = []*snap.Info{}
   207  
   208  	req, err := http.NewRequest("GET", "/v2/find?name=foo*", nil)
   209  	c.Assert(err, check.IsNil)
   210  
   211  	_ = s.syncReq(c, req, nil)
   212  
   213  	c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo", Prefix: true})
   214  }
   215  
   216  func (s *findSuite) TestFindSection(c *check.C) {
   217  	s.daemon(c)
   218  
   219  	s.rsnaps = []*snap.Info{}
   220  
   221  	req, err := http.NewRequest("GET", "/v2/find?q=foo&section=bar", nil)
   222  	c.Assert(err, check.IsNil)
   223  
   224  	_ = s.syncReq(c, req, nil)
   225  
   226  	c.Check(s.storeSearch, check.DeepEquals, store.Search{
   227  		Query:    "foo",
   228  		Category: "bar",
   229  	})
   230  }
   231  
   232  func (s *findSuite) TestFindScope(c *check.C) {
   233  	s.daemon(c)
   234  
   235  	s.rsnaps = []*snap.Info{}
   236  
   237  	req, err := http.NewRequest("GET", "/v2/find?q=foo&scope=creep", nil)
   238  	c.Assert(err, check.IsNil)
   239  
   240  	_ = s.syncReq(c, req, nil)
   241  
   242  	c.Check(s.storeSearch, check.DeepEquals, store.Search{
   243  		Query: "foo",
   244  		Scope: "creep",
   245  	})
   246  }
   247  
   248  func (s *findSuite) TestFindCommonID(c *check.C) {
   249  	s.daemon(c)
   250  
   251  	s.rsnaps = []*snap.Info{{
   252  		SideInfo: snap.SideInfo{
   253  			RealName: "store",
   254  		},
   255  		Publisher: snap.StoreAccount{
   256  			ID:          "foo-id",
   257  			Username:    "foo",
   258  			DisplayName: "Foo",
   259  			Validation:  "unproven",
   260  		},
   261  		CommonIDs: []string{"org.foo"},
   262  	}}
   263  	s.mockSnap(c, "name: store\nversion: 1.0")
   264  
   265  	req, err := http.NewRequest("GET", "/v2/find?name=foo", nil)
   266  	c.Assert(err, check.IsNil)
   267  
   268  	rsp := s.syncReq(c, req, nil)
   269  
   270  	snaps := snapList(rsp.Result)
   271  	c.Assert(snaps, check.HasLen, 1)
   272  	c.Check(snaps[0]["common-ids"], check.DeepEquals, []interface{}{"org.foo"})
   273  }
   274  
   275  func (s *findSuite) TestFindByCommonID(c *check.C) {
   276  	s.daemon(c)
   277  
   278  	s.rsnaps = []*snap.Info{{
   279  		SideInfo: snap.SideInfo{
   280  			RealName: "store",
   281  		},
   282  		Publisher: snap.StoreAccount{
   283  			ID:          "foo-id",
   284  			Username:    "foo",
   285  			DisplayName: "Foo",
   286  			Validation:  "unproven",
   287  		},
   288  		CommonIDs: []string{"org.foo"},
   289  	}}
   290  	s.mockSnap(c, "name: store\nversion: 1.0")
   291  
   292  	req, err := http.NewRequest("GET", "/v2/find?common-id=org.foo", nil)
   293  	c.Assert(err, check.IsNil)
   294  
   295  	rsp := s.syncReq(c, req, nil)
   296  
   297  	snaps := snapList(rsp.Result)
   298  	c.Assert(snaps, check.HasLen, 1)
   299  	c.Check(s.storeSearch, check.DeepEquals, store.Search{CommonID: "org.foo"})
   300  }
   301  
   302  func (s *findSuite) TestFindOne(c *check.C) {
   303  	s.daemon(c)
   304  
   305  	s.rsnaps = []*snap.Info{{
   306  		SideInfo: snap.SideInfo{
   307  			RealName: "store",
   308  		},
   309  		Base: "base0",
   310  		Publisher: snap.StoreAccount{
   311  			ID:          "foo-id",
   312  			Username:    "foo",
   313  			DisplayName: "Foo",
   314  			Validation:  "verified",
   315  		},
   316  		Channels: map[string]*snap.ChannelSnapInfo{
   317  			"stable": {
   318  				Revision: snap.R(42),
   319  			},
   320  		},
   321  	}}
   322  	s.mockSnap(c, "name: store\nversion: 1.0")
   323  
   324  	req, err := http.NewRequest("GET", "/v2/find?name=foo", nil)
   325  	c.Assert(err, check.IsNil)
   326  
   327  	rsp := s.syncReq(c, req, nil)
   328  
   329  	c.Check(s.storeSearch, check.DeepEquals, store.Search{})
   330  
   331  	snaps := snapList(rsp.Result)
   332  	c.Assert(snaps, check.HasLen, 1)
   333  	c.Check(snaps[0]["name"], check.Equals, "store")
   334  	c.Check(snaps[0]["base"], check.Equals, "base0")
   335  	c.Check(snaps[0]["publisher"], check.DeepEquals, map[string]interface{}{
   336  		"id":           "foo-id",
   337  		"username":     "foo",
   338  		"display-name": "Foo",
   339  		"validation":   "verified",
   340  	})
   341  	m := snaps[0]["channels"].(map[string]interface{})["stable"].(map[string]interface{})
   342  
   343  	c.Check(m["revision"], check.Equals, "42")
   344  }
   345  
   346  func (s *findSuite) TestFindOneNotFound(c *check.C) {
   347  	s.daemon(c)
   348  
   349  	s.err = store.ErrSnapNotFound
   350  	s.mockSnap(c, "name: store\nversion: 1.0")
   351  
   352  	req, err := http.NewRequest("GET", "/v2/find?name=foo", nil)
   353  	c.Assert(err, check.IsNil)
   354  
   355  	rsp := s.errorReq(c, req, nil)
   356  
   357  	c.Check(s.storeSearch, check.DeepEquals, store.Search{})
   358  	c.Check(rsp.Status, check.Equals, 404)
   359  }
   360  
   361  func (s *findSuite) TestFindOneWithAuth(c *check.C) {
   362  	d := s.daemon(c)
   363  
   364  	state := d.Overlord().State()
   365  	state.Lock()
   366  	user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
   367  	state.Unlock()
   368  	c.Check(err, check.IsNil)
   369  
   370  	req, err := http.NewRequest("GET", "/v2/find?q=name:gfoo", nil)
   371  	c.Assert(err, check.IsNil)
   372  
   373  	c.Assert(s.user, check.IsNil)
   374  
   375  	_ = s.syncReq(c, req, user)
   376  
   377  	// ensure user was set
   378  	c.Assert(s.user, check.DeepEquals, user)
   379  }
   380  
   381  func (s *findSuite) TestFindRefreshNotOther(c *check.C) {
   382  	s.daemon(c)
   383  
   384  	for _, other := range []string{"name", "q", "common-id"} {
   385  		req, err := http.NewRequest("GET", "/v2/find?select=refresh&"+other+"=foo*", nil)
   386  		c.Assert(err, check.IsNil)
   387  
   388  		rspe := s.errorReq(c, req, nil)
   389  		c.Check(rspe.Status, check.Equals, 400)
   390  		c.Check(rspe.Message, check.Equals, "cannot use '"+other+"' with 'select=refresh'")
   391  	}
   392  }
   393  
   394  func (s *findSuite) TestFindNotTogether(c *check.C) {
   395  	s.daemon(c)
   396  
   397  	queries := map[string]string{"q": "foo", "name": "foo*", "common-id": "foo"}
   398  	for ki, vi := range queries {
   399  		for kj, vj := range queries {
   400  			if ki == kj {
   401  				continue
   402  			}
   403  
   404  			req, err := http.NewRequest("GET", fmt.Sprintf("/v2/find?%s=%s&%s=%s", ki, vi, kj, vj), nil)
   405  			c.Assert(err, check.IsNil)
   406  
   407  			rspe := s.errorReq(c, req, nil)
   408  			c.Check(rspe.Status, check.Equals, 400)
   409  			exp1 := "cannot use '" + ki + "' and '" + kj + "' together"
   410  			exp2 := "cannot use '" + kj + "' and '" + ki + "' together"
   411  			c.Check(rspe.Message, check.Matches, exp1+"|"+exp2)
   412  		}
   413  	}
   414  }
   415  
   416  func (s *findSuite) TestFindBadQueryReturnsCorrectErrorKind(c *check.C) {
   417  	s.daemon(c)
   418  
   419  	s.err = store.ErrBadQuery
   420  	req, err := http.NewRequest("GET", "/v2/find?q=return-bad-query-please", nil)
   421  	c.Assert(err, check.IsNil)
   422  
   423  	rspe := s.errorReq(c, req, nil)
   424  	c.Check(rspe.Status, check.Equals, 400)
   425  	c.Check(rspe.Message, check.Matches, "bad query")
   426  	c.Check(rspe.Kind, check.Equals, client.ErrorKindBadQuery)
   427  }
   428  
   429  func (s *findSuite) TestFindNetworkErrorsReturnCorrectErrorKind(c *check.C) {
   430  	s.daemon(c)
   431  
   432  	req, err := http.NewRequest("GET", "/v2/find?q=query", nil)
   433  	c.Assert(err, check.IsNil)
   434  
   435  	pne := &httputil.PersistentNetworkError{Err: errors.New("problem")}
   436  	neTout := fakeNetError{message: "net problem", timeout: true}
   437  	dnse := &net.DNSError{Name: "store", IsTemporary: true}
   438  	uDNSe := &url.Error{
   439  		Op:  "Get",
   440  		URL: "http://...",
   441  		Err: &net.OpError{
   442  			Op:  "dial",
   443  			Net: "tcp",
   444  			Err: dnse,
   445  		},
   446  	}
   447  
   448  	tests := []struct {
   449  		err      error
   450  		kind     client.ErrorKind
   451  		expected string
   452  	}{
   453  		{pne, client.ErrorKindDNSFailure, "persistent network error: problem"},
   454  		{neTout, client.ErrorKindNetworkTimeout, "net problem"},
   455  		{uDNSe, client.ErrorKindDNSFailure, dnse.Error()},
   456  	}
   457  
   458  	for _, t := range tests {
   459  		s.err = t.err
   460  
   461  		rspe := s.errorReq(c, req, nil)
   462  		c.Check(rspe.Status, check.Equals, 400)
   463  		c.Check(rspe.Message, check.Equals, t.expected)
   464  		c.Check(rspe.Kind, check.Equals, t.kind)
   465  	}
   466  }
   467  
   468  func (s *findSuite) TestFindPriced(c *check.C) {
   469  	s.daemon(c)
   470  
   471  	s.suggestedCurrency = "GBP"
   472  
   473  	s.rsnaps = []*snap.Info{{
   474  		SnapType: snap.TypeApp,
   475  		Version:  "v2",
   476  		Prices: map[string]float64{
   477  			"GBP": 1.23,
   478  			"EUR": 2.34,
   479  		},
   480  		MustBuy: true,
   481  		SideInfo: snap.SideInfo{
   482  			RealName: "banana",
   483  		},
   484  		Publisher: snap.StoreAccount{
   485  			ID:          "foo-id",
   486  			Username:    "foo",
   487  			DisplayName: "Foo",
   488  			Validation:  "unproven",
   489  		},
   490  	}}
   491  
   492  	req, err := http.NewRequest("GET", "/v2/find?q=banana&channel=stable", nil)
   493  	c.Assert(err, check.IsNil)
   494  	rsp := s.syncReq(c, req, nil)
   495  
   496  	snaps := snapList(rsp.Result)
   497  	c.Assert(snaps, check.HasLen, 1)
   498  
   499  	snap := snaps[0]
   500  	c.Check(snap["name"], check.Equals, "banana")
   501  	c.Check(snap["prices"], check.DeepEquals, map[string]interface{}{
   502  		"EUR": 2.34,
   503  		"GBP": 1.23,
   504  	})
   505  	c.Check(snap["status"], check.Equals, "priced")
   506  
   507  	c.Check(rsp.SuggestedCurrency, check.Equals, "GBP")
   508  }
   509  
   510  func (s *findSuite) TestFindScreenshotted(c *check.C) {
   511  	s.daemon(c)
   512  
   513  	s.rsnaps = []*snap.Info{{
   514  		SnapType: snap.TypeApp,
   515  		Version:  "v2",
   516  		Media: []snap.MediaInfo{
   517  			{
   518  				Type:   "screenshot",
   519  				URL:    "http://example.com/screenshot.png",
   520  				Width:  800,
   521  				Height: 1280,
   522  			},
   523  			{
   524  				Type: "screenshot",
   525  				URL:  "http://example.com/screenshot2.png",
   526  			},
   527  		},
   528  		MustBuy: true,
   529  		SideInfo: snap.SideInfo{
   530  			RealName: "test-screenshot",
   531  		},
   532  		Publisher: snap.StoreAccount{
   533  			ID:          "foo-id",
   534  			Username:    "foo",
   535  			DisplayName: "Foo",
   536  			Validation:  "unproven",
   537  		},
   538  	}}
   539  
   540  	req, err := http.NewRequest("GET", "/v2/find?q=test-screenshot", nil)
   541  	c.Assert(err, check.IsNil)
   542  	rsp := s.syncReq(c, req, nil)
   543  
   544  	snaps := snapList(rsp.Result)
   545  	c.Assert(snaps, check.HasLen, 1)
   546  
   547  	c.Check(snaps[0]["name"], check.Equals, "test-screenshot")
   548  	c.Check(snaps[0]["media"], check.DeepEquals, []interface{}{
   549  		map[string]interface{}{
   550  			"type":   "screenshot",
   551  			"url":    "http://example.com/screenshot.png",
   552  			"width":  float64(800),
   553  			"height": float64(1280),
   554  		},
   555  		map[string]interface{}{
   556  			"type": "screenshot",
   557  			"url":  "http://example.com/screenshot2.png",
   558  		},
   559  	})
   560  }
   561  
   562  func (s *findSuite) TestSnapsStoreConfinement(c *check.C) {
   563  	s.daemon(c)
   564  
   565  	s.rsnaps = []*snap.Info{
   566  		{
   567  			// no explicit confinement in this one
   568  			SideInfo: snap.SideInfo{
   569  				RealName: "foo",
   570  			},
   571  		},
   572  		{
   573  			Confinement: snap.StrictConfinement,
   574  			SideInfo: snap.SideInfo{
   575  				RealName: "bar",
   576  			},
   577  		},
   578  		{
   579  			Confinement: snap.DevModeConfinement,
   580  			SideInfo: snap.SideInfo{
   581  				RealName: "baz",
   582  			},
   583  		},
   584  	}
   585  
   586  	req, err := http.NewRequest("GET", "/v2/find", nil)
   587  	c.Assert(err, check.IsNil)
   588  
   589  	rsp := s.syncReq(c, req, nil)
   590  
   591  	snaps := snapList(rsp.Result)
   592  	c.Assert(snaps, check.HasLen, 3)
   593  
   594  	for i, ss := range [][2]string{
   595  		{"foo", string(snap.StrictConfinement)},
   596  		{"bar", string(snap.StrictConfinement)},
   597  		{"baz", string(snap.DevModeConfinement)},
   598  	} {
   599  		name, mode := ss[0], ss[1]
   600  		c.Check(snaps[i]["name"], check.Equals, name, check.Commentf(name))
   601  		c.Check(snaps[i]["confinement"], check.Equals, mode, check.Commentf(name))
   602  	}
   603  }