github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api_snaps_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  	"bytes"
    24  	"context"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	//"io/ioutil"
    29  	//"mime/multipart"
    30  	"net/http"
    31  	//"net/http/httptest"
    32  	"net/url"
    33  	"os"
    34  	"path/filepath"
    35  	"strings"
    36  
    37  	"gopkg.in/check.v1"
    38  
    39  	//"github.com/snapcore/snapd/arch"
    40  	//"github.com/snapcore/snapd/asserts"
    41  	//"github.com/snapcore/snapd/asserts/assertstest"
    42  	//"github.com/snapcore/snapd/client"
    43  	"github.com/snapcore/snapd/daemon"
    44  	//"github.com/snapcore/snapd/dirs"
    45  	"github.com/snapcore/snapd/overlord/assertstate"
    46  	//"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
    47  	"github.com/snapcore/snapd/overlord/auth"
    48  	"github.com/snapcore/snapd/overlord/healthstate"
    49  	"github.com/snapcore/snapd/overlord/snapstate"
    50  	"github.com/snapcore/snapd/overlord/state"
    51  	//"github.com/snapcore/snapd/sandbox"
    52  	"github.com/snapcore/snapd/snap"
    53  	//"github.com/snapcore/snapd/snap/channel"
    54  	//"github.com/snapcore/snapd/snap/snaptest"
    55  	"github.com/snapcore/snapd/store"
    56  	"github.com/snapcore/snapd/testutil"
    57  )
    58  
    59  type snapsSuite struct {
    60  	apiBaseSuite
    61  }
    62  
    63  var _ = check.Suite(&snapsSuite{})
    64  
    65  func (s *snapsSuite) TestSnapsInfoIntegration(c *check.C) {
    66  	s.checkSnapsInfoIntegration(c, false, nil)
    67  }
    68  
    69  func (s *snapsSuite) TestSnapsInfoIntegrationSome(c *check.C) {
    70  	s.checkSnapsInfoIntegration(c, false, []string{"foo", "baz"})
    71  }
    72  
    73  func (s *snapsSuite) TestSnapsInfoIntegrationAll(c *check.C) {
    74  	s.checkSnapsInfoIntegration(c, true, nil)
    75  }
    76  
    77  func (s *snapsSuite) TestSnapsInfoIntegrationAllSome(c *check.C) {
    78  	s.checkSnapsInfoIntegration(c, true, []string{"foo", "baz"})
    79  }
    80  
    81  func snapList(rawSnaps interface{}) []map[string]interface{} {
    82  	snaps := make([]map[string]interface{}, len(rawSnaps.([]*json.RawMessage)))
    83  	for i, raw := range rawSnaps.([]*json.RawMessage) {
    84  		err := json.Unmarshal([]byte(*raw), &snaps[i])
    85  		if err != nil {
    86  			panic(err)
    87  		}
    88  	}
    89  	return snaps
    90  }
    91  
    92  func (s *snapsSuite) checkSnapsInfoIntegration(c *check.C, all bool, names []string) {
    93  	d := s.daemon(c)
    94  
    95  	type tsnap struct {
    96  		name   string
    97  		dev    string
    98  		ver    string
    99  		rev    int
   100  		active bool
   101  
   102  		wanted bool
   103  	}
   104  
   105  	tsnaps := []tsnap{
   106  		{name: "foo", dev: "bar", ver: "v0.9", rev: 1},
   107  		{name: "foo", dev: "bar", ver: "v1", rev: 5, active: true},
   108  		{name: "bar", dev: "baz", ver: "v2", rev: 10, active: true},
   109  		{name: "baz", dev: "qux", ver: "v3", rev: 15, active: true},
   110  		{name: "qux", dev: "mip", ver: "v4", rev: 20, active: true},
   111  	}
   112  	numExpected := 0
   113  
   114  	for _, snp := range tsnaps {
   115  		if all || snp.active {
   116  			if len(names) == 0 {
   117  				numExpected++
   118  				snp.wanted = true
   119  			}
   120  			for _, n := range names {
   121  				if snp.name == n {
   122  					numExpected++
   123  					snp.wanted = true
   124  					break
   125  				}
   126  			}
   127  		}
   128  		s.mkInstalledInState(c, d, snp.name, snp.dev, snp.ver, snap.R(snp.rev), snp.active, "")
   129  	}
   130  
   131  	q := url.Values{}
   132  	if all {
   133  		q.Set("select", "all")
   134  	}
   135  	if len(names) > 0 {
   136  		q.Set("snaps", strings.Join(names, ","))
   137  	}
   138  	req, err := http.NewRequest("GET", "/v2/snaps?"+q.Encode(), nil)
   139  	c.Assert(err, check.IsNil)
   140  
   141  	rsp, ok := s.req(c, req, nil).(*daemon.Resp)
   142  	c.Assert(ok, check.Equals, true)
   143  
   144  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   145  	c.Check(rsp.Status, check.Equals, 200)
   146  	c.Check(rsp.Result, check.NotNil)
   147  
   148  	snaps := snapList(rsp.Result)
   149  	c.Check(snaps, check.HasLen, numExpected)
   150  
   151  	for _, s := range tsnaps {
   152  		if !((all || s.active) && s.wanted) {
   153  			continue
   154  		}
   155  		var got map[string]interface{}
   156  		for _, got = range snaps {
   157  			if got["name"].(string) == s.name && got["revision"].(string) == snap.R(s.rev).String() {
   158  				break
   159  			}
   160  		}
   161  		c.Check(got["name"], check.Equals, s.name)
   162  		c.Check(got["version"], check.Equals, s.ver)
   163  		c.Check(got["revision"], check.Equals, snap.R(s.rev).String())
   164  		c.Check(got["developer"], check.Equals, s.dev)
   165  		c.Check(got["confinement"], check.Equals, "strict")
   166  	}
   167  }
   168  
   169  func (s *snapsSuite) TestSnapsInfoOnlyLocal(c *check.C) {
   170  	d := s.daemon(c)
   171  
   172  	s.rsnaps = []*snap.Info{{
   173  		SideInfo: snap.SideInfo{
   174  			RealName: "store",
   175  		},
   176  		Publisher: snap.StoreAccount{
   177  			ID:          "foo-id",
   178  			Username:    "foo",
   179  			DisplayName: "Foo",
   180  			Validation:  "unproven",
   181  		},
   182  	}}
   183  	s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
   184  	st := d.Overlord().State()
   185  	st.Lock()
   186  	st.Set("health", map[string]healthstate.HealthState{
   187  		"local": {Status: healthstate.OkayStatus},
   188  	})
   189  	st.Unlock()
   190  
   191  	req, err := http.NewRequest("GET", "/v2/snaps?sources=local", nil)
   192  	c.Assert(err, check.IsNil)
   193  
   194  	rsp := s.req(c, req, nil).(*daemon.Resp)
   195  
   196  	c.Assert(rsp.Sources, check.DeepEquals, []string{"local"})
   197  
   198  	snaps := snapList(rsp.Result)
   199  	c.Assert(snaps, check.HasLen, 1)
   200  	c.Assert(snaps[0]["name"], check.Equals, "local")
   201  	c.Check(snaps[0]["health"], check.DeepEquals, map[string]interface{}{
   202  		"status":    "okay",
   203  		"revision":  "unset",
   204  		"timestamp": "0001-01-01T00:00:00Z",
   205  	})
   206  }
   207  
   208  func (s *snapsSuite) TestSnapsInfoAllMixedPublishers(c *check.C) {
   209  	d := s.daemon(c)
   210  
   211  	// the first 'local' is from a 'local' snap
   212  	s.mkInstalledInState(c, d, "local", "", "v1", snap.R(-1), false, "")
   213  	s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(1), false, "")
   214  	s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(2), true, "")
   215  
   216  	req, err := http.NewRequest("GET", "/v2/snaps?select=all", nil)
   217  	c.Assert(err, check.IsNil)
   218  	rsp := s.req(c, req, nil).(*daemon.Resp)
   219  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   220  
   221  	snaps := snapList(rsp.Result)
   222  	c.Assert(snaps, check.HasLen, 3)
   223  
   224  	publisher := map[string]interface{}{
   225  		"id":           "foo-id",
   226  		"username":     "foo",
   227  		"display-name": "Foo",
   228  		"validation":   "unproven",
   229  	}
   230  
   231  	c.Check(snaps[0]["publisher"], check.IsNil)
   232  	c.Check(snaps[1]["publisher"], check.DeepEquals, publisher)
   233  	c.Check(snaps[2]["publisher"], check.DeepEquals, publisher)
   234  }
   235  
   236  func (s *snapsSuite) TestSnapsInfoAll(c *check.C) {
   237  	d := s.daemon(c)
   238  
   239  	s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(1), false, "")
   240  	s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(2), false, "")
   241  	s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(3), true, "")
   242  	s.mkInstalledInState(c, d, "local_foo", "foo", "v4", snap.R(4), true, "")
   243  	brokenInfo := s.mkInstalledInState(c, d, "local_bar", "foo", "v5", snap.R(5), true, "")
   244  	// make sure local_bar is 'broken'
   245  	err := os.Remove(filepath.Join(brokenInfo.MountDir(), "meta", "snap.yaml"))
   246  	c.Assert(err, check.IsNil)
   247  
   248  	expectedHappy := map[string]bool{
   249  		"local":     true,
   250  		"local_foo": true,
   251  		"local_bar": true,
   252  	}
   253  	for _, t := range []struct {
   254  		q        string
   255  		numSnaps int
   256  		typ      daemon.ResponseType
   257  	}{
   258  		{"?select=enabled", 3, "sync"},
   259  		{`?select=`, 3, "sync"},
   260  		{"", 3, "sync"},
   261  		{"?select=all", 5, "sync"},
   262  		{"?select=invalid-field", 0, "error"},
   263  	} {
   264  		c.Logf("trying: %v", t)
   265  		req, err := http.NewRequest("GET", fmt.Sprintf("/v2/snaps%s", t.q), nil)
   266  		c.Assert(err, check.IsNil)
   267  		rsp := s.req(c, req, nil).(*daemon.Resp)
   268  		c.Assert(rsp.Type, check.Equals, t.typ)
   269  
   270  		if rsp.Type != "error" {
   271  			snaps := snapList(rsp.Result)
   272  			c.Assert(snaps, check.HasLen, t.numSnaps)
   273  			seen := map[string]bool{}
   274  			for _, s := range snaps {
   275  				seen[s["name"].(string)] = true
   276  			}
   277  			c.Assert(seen, check.DeepEquals, expectedHappy)
   278  		}
   279  	}
   280  }
   281  
   282  func (s *snapsSuite) TestSnapsInfoOnlyStore(c *check.C) {
   283  	d := s.daemon(c)
   284  
   285  	s.suggestedCurrency = "EUR"
   286  
   287  	s.rsnaps = []*snap.Info{{
   288  		SideInfo: snap.SideInfo{
   289  			RealName: "store",
   290  		},
   291  		Publisher: snap.StoreAccount{
   292  			ID:          "foo-id",
   293  			Username:    "foo",
   294  			DisplayName: "Foo",
   295  			Validation:  "unproven",
   296  		},
   297  	}}
   298  	s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
   299  
   300  	req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil)
   301  	c.Assert(err, check.IsNil)
   302  
   303  	rsp := s.req(c, req, nil).(*daemon.Resp)
   304  
   305  	c.Assert(rsp.Sources, check.DeepEquals, []string{"store"})
   306  
   307  	snaps := snapList(rsp.Result)
   308  	c.Assert(snaps, check.HasLen, 1)
   309  	c.Assert(snaps[0]["name"], check.Equals, "store")
   310  	c.Check(snaps[0]["prices"], check.IsNil)
   311  
   312  	c.Check(rsp.SuggestedCurrency, check.Equals, "EUR")
   313  }
   314  
   315  func (s *snapsSuite) TestSnapsInfoStoreWithAuth(c *check.C) {
   316  	d := s.daemon(c)
   317  
   318  	state := d.Overlord().State()
   319  	state.Lock()
   320  	user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"})
   321  	state.Unlock()
   322  	c.Check(err, check.IsNil)
   323  
   324  	req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil)
   325  	c.Assert(err, check.IsNil)
   326  
   327  	c.Assert(s.user, check.IsNil)
   328  
   329  	_ = s.req(c, req, user).(*daemon.Resp)
   330  
   331  	// ensure user was set
   332  	c.Assert(s.user, check.DeepEquals, user)
   333  }
   334  
   335  func (s *snapsSuite) TestSnapsInfoLocalAndStore(c *check.C) {
   336  	d := s.daemon(c)
   337  
   338  	s.rsnaps = []*snap.Info{{
   339  		Version: "v42",
   340  		SideInfo: snap.SideInfo{
   341  			RealName: "remote",
   342  		},
   343  		Publisher: snap.StoreAccount{
   344  			ID:          "foo-id",
   345  			Username:    "foo",
   346  			DisplayName: "Foo",
   347  			Validation:  "unproven",
   348  		},
   349  	}}
   350  	s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
   351  
   352  	req, err := http.NewRequest("GET", "/v2/snaps?sources=local,store", nil)
   353  	c.Assert(err, check.IsNil)
   354  
   355  	rsp := s.req(c, req, nil).(*daemon.Resp)
   356  
   357  	// presence of 'store' in sources bounces request over to /find
   358  	c.Assert(rsp.Sources, check.DeepEquals, []string{"store"})
   359  
   360  	snaps := snapList(rsp.Result)
   361  	c.Assert(snaps, check.HasLen, 1)
   362  	c.Check(snaps[0]["version"], check.Equals, "v42")
   363  
   364  	// as does a 'q'
   365  	req, err = http.NewRequest("GET", "/v2/snaps?q=what", nil)
   366  	c.Assert(err, check.IsNil)
   367  	rsp = s.req(c, req, nil).(*daemon.Resp)
   368  	snaps = snapList(rsp.Result)
   369  	c.Assert(snaps, check.HasLen, 1)
   370  	c.Check(snaps[0]["version"], check.Equals, "v42")
   371  
   372  	// otherwise, local only
   373  	req, err = http.NewRequest("GET", "/v2/snaps", nil)
   374  	c.Assert(err, check.IsNil)
   375  	rsp = s.req(c, req, nil).(*daemon.Resp)
   376  	snaps = snapList(rsp.Result)
   377  	c.Assert(snaps, check.HasLen, 1)
   378  	c.Check(snaps[0]["version"], check.Equals, "v1")
   379  }
   380  
   381  func (s *snapsSuite) TestSnapsInfoDefaultSources(c *check.C) {
   382  	d := s.daemon(c)
   383  
   384  	s.rsnaps = []*snap.Info{{
   385  		SideInfo: snap.SideInfo{
   386  			RealName: "remote",
   387  		},
   388  		Publisher: snap.StoreAccount{
   389  			ID:          "foo-id",
   390  			Username:    "foo",
   391  			DisplayName: "Foo",
   392  			Validation:  "unproven",
   393  		},
   394  	}}
   395  	s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
   396  
   397  	req, err := http.NewRequest("GET", "/v2/snaps", nil)
   398  	c.Assert(err, check.IsNil)
   399  
   400  	rsp := s.req(c, req, nil).(*daemon.Resp)
   401  
   402  	c.Assert(rsp.Sources, check.DeepEquals, []string{"local"})
   403  	snaps := snapList(rsp.Result)
   404  	c.Assert(snaps, check.HasLen, 1)
   405  }
   406  
   407  func (s *snapsSuite) TestSnapsInfoFilterRemote(c *check.C) {
   408  	s.daemon(c)
   409  
   410  	s.rsnaps = nil
   411  
   412  	req, err := http.NewRequest("GET", "/v2/snaps?q=foo&sources=store", nil)
   413  	c.Assert(err, check.IsNil)
   414  
   415  	rsp := s.req(c, req, nil).(*daemon.Resp)
   416  
   417  	c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo"})
   418  
   419  	c.Assert(rsp.Result, check.NotNil)
   420  }
   421  
   422  func (s *snapsSuite) TestPostSnapsVerifyMultiSnapInstruction(c *check.C) {
   423  	s.daemonWithOverlordMockAndStore(c)
   424  
   425  	buf := strings.NewReader(`{"action": "install","snaps":["ubuntu-core"]}`)
   426  	req, err := http.NewRequest("POST", "/v2/snaps", buf)
   427  	c.Assert(err, check.IsNil)
   428  	req.Header.Set("Content-Type", "application/json")
   429  
   430  	rsp := s.req(c, req, nil).(*daemon.Resp)
   431  
   432  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   433  	c.Check(rsp.Status, check.Equals, 400)
   434  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, testutil.Contains, `cannot install "ubuntu-core", please use "core" instead`)
   435  }
   436  
   437  func (s *snapsSuite) TestPostSnapsUnsupportedMultiOp(c *check.C) {
   438  	s.daemonWithOverlordMockAndStore(c)
   439  
   440  	buf := strings.NewReader(`{"action": "switch","snaps":["foo"]}`)
   441  	req, err := http.NewRequest("POST", "/v2/snaps", buf)
   442  	c.Assert(err, check.IsNil)
   443  	req.Header.Set("Content-Type", "application/json")
   444  
   445  	rsp := s.req(c, req, nil).(*daemon.Resp)
   446  
   447  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   448  	c.Check(rsp.Status, check.Equals, 400)
   449  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, testutil.Contains, `unsupported multi-snap operation "switch"`)
   450  }
   451  
   452  func (s *snapsSuite) TestPostSnapsNoWeirdses(c *check.C) {
   453  	s.daemonWithOverlordMockAndStore(c)
   454  
   455  	// one could add more actions here ... 🤷
   456  	for _, action := range []string{"install", "refresh", "remove"} {
   457  		for weird, v := range map[string]string{
   458  			"channel":      `"beta"`,
   459  			"revision":     `"1"`,
   460  			"devmode":      "true",
   461  			"jailmode":     "true",
   462  			"cohort-key":   `"what"`,
   463  			"leave-cohort": "true",
   464  			"purge":        "true",
   465  		} {
   466  			buf := strings.NewReader(fmt.Sprintf(`{"action": "%s","snaps":["foo","bar"], "%s": %s}`, action, weird, v))
   467  			req, err := http.NewRequest("POST", "/v2/snaps", buf)
   468  			c.Assert(err, check.IsNil)
   469  			req.Header.Set("Content-Type", "application/json")
   470  
   471  			rsp := s.req(c, req, nil).(*daemon.Resp)
   472  
   473  			c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   474  			c.Check(rsp.Status, check.Equals, 400)
   475  			c.Check(rsp.Result.(*daemon.ErrorResult).Message, testutil.Contains, `unsupported option provided for multi-snap operation`)
   476  		}
   477  	}
   478  }
   479  
   480  func (s *snapsSuite) TestPostSnapsOp(c *check.C) {
   481  	s.testPostSnapsOp(c, "application/json")
   482  }
   483  
   484  func (s *snapsSuite) TestPostSnapsOpMoreComplexContentType(c *check.C) {
   485  	s.testPostSnapsOp(c, "application/json; charset=utf-8")
   486  }
   487  
   488  func (s *snapsSuite) testPostSnapsOp(c *check.C, contentType string) {
   489  	defer daemon.MockAssertstateRefreshSnapDeclarations(func(*state.State, int) error { return nil })()
   490  	defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
   491  		c.Check(names, check.HasLen, 0)
   492  		t := s.NewTask("fake-refresh-all", "Refreshing everything")
   493  		return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil
   494  	})()
   495  
   496  	d := s.daemonWithOverlordMockAndStore(c)
   497  
   498  	buf := bytes.NewBufferString(`{"action": "refresh"}`)
   499  	req, err := http.NewRequest("POST", "/v2/snaps", buf)
   500  	c.Assert(err, check.IsNil)
   501  	req.Header.Set("Content-Type", contentType)
   502  
   503  	rsp, ok := s.req(c, req, nil).(*daemon.Resp)
   504  	c.Assert(ok, check.Equals, true)
   505  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeAsync)
   506  
   507  	st := d.Overlord().State()
   508  	st.Lock()
   509  	defer st.Unlock()
   510  	chg := st.Change(rsp.Change)
   511  	c.Check(chg.Summary(), check.Equals, `Refresh snaps "fake1", "fake2"`)
   512  	var apiData map[string]interface{}
   513  	c.Check(chg.Get("api-data", &apiData), check.IsNil)
   514  	c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"})
   515  }
   516  
   517  func (s *snapsSuite) TestPostSnapsOpInvalidCharset(c *check.C) {
   518  	s.daemon(c)
   519  
   520  	buf := bytes.NewBufferString(`{"action": "refresh"}`)
   521  	req, err := http.NewRequest("POST", "/v2/snaps", buf)
   522  	c.Assert(err, check.IsNil)
   523  	req.Header.Set("Content-Type", "application/json; charset=iso-8859-1")
   524  
   525  	rsp := s.req(c, req, nil).(*daemon.Resp)
   526  	c.Check(rsp.Status, check.Equals, 400)
   527  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, testutil.Contains, "unknown charset in content type")
   528  }
   529  
   530  func (s *snapsSuite) TestRefreshAll(c *check.C) {
   531  	refreshSnapDecls := false
   532  	defer daemon.MockAssertstateRefreshSnapDeclarations(func(s *state.State, userID int) error {
   533  		refreshSnapDecls = true
   534  		return assertstate.RefreshSnapDeclarations(s, userID)
   535  	})()
   536  
   537  	d := s.daemon(c)
   538  
   539  	for _, tst := range []struct {
   540  		snaps []string
   541  		msg   string
   542  	}{
   543  		{nil, "Refresh all snaps: no updates"},
   544  		{[]string{"fake"}, `Refresh snap "fake"`},
   545  		{[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`},
   546  	} {
   547  		refreshSnapDecls = false
   548  
   549  		defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
   550  			c.Check(names, check.HasLen, 0)
   551  			t := s.NewTask("fake-refresh-all", "Refreshing everything")
   552  			return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil
   553  		})()
   554  
   555  		inst := &daemon.SnapInstruction{Action: "refresh"}
   556  		st := d.Overlord().State()
   557  		st.Lock()
   558  		res, err := inst.DispatchForMany()(inst, st)
   559  		st.Unlock()
   560  		c.Assert(err, check.IsNil)
   561  		c.Check(res.Summary, check.Equals, tst.msg)
   562  		c.Check(refreshSnapDecls, check.Equals, true)
   563  	}
   564  }
   565  
   566  func (s *snapsSuite) TestRefreshAllNoChanges(c *check.C) {
   567  	refreshSnapDecls := false
   568  	defer daemon.MockAssertstateRefreshSnapDeclarations(func(s *state.State, userID int) error {
   569  		refreshSnapDecls = true
   570  		return assertstate.RefreshSnapDeclarations(s, userID)
   571  	})()
   572  
   573  	defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
   574  		c.Check(names, check.HasLen, 0)
   575  		return nil, nil, nil
   576  	})()
   577  
   578  	d := s.daemon(c)
   579  	inst := &daemon.SnapInstruction{Action: "refresh"}
   580  	st := d.Overlord().State()
   581  	st.Lock()
   582  	res, err := inst.DispatchForMany()(inst, st)
   583  	st.Unlock()
   584  	c.Assert(err, check.IsNil)
   585  	c.Check(res.Summary, check.Equals, `Refresh all snaps: no updates`)
   586  	c.Check(refreshSnapDecls, check.Equals, true)
   587  }
   588  
   589  func (s *snapsSuite) TestRefreshMany(c *check.C) {
   590  	refreshSnapDecls := false
   591  	defer daemon.MockAssertstateRefreshSnapDeclarations(func(s *state.State, userID int) error {
   592  		refreshSnapDecls = true
   593  		return nil
   594  	})()
   595  
   596  	defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
   597  		c.Check(names, check.HasLen, 2)
   598  		t := s.NewTask("fake-refresh-2", "Refreshing two")
   599  		return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
   600  	})()
   601  
   602  	d := s.daemon(c)
   603  	inst := &daemon.SnapInstruction{Action: "refresh", Snaps: []string{"foo", "bar"}}
   604  	st := d.Overlord().State()
   605  	st.Lock()
   606  	res, err := inst.DispatchForMany()(inst, st)
   607  	st.Unlock()
   608  	c.Assert(err, check.IsNil)
   609  	c.Check(res.Summary, check.Equals, `Refresh snaps "foo", "bar"`)
   610  	c.Check(res.Affected, check.DeepEquals, inst.Snaps)
   611  	c.Check(refreshSnapDecls, check.Equals, true)
   612  }
   613  
   614  func (s *snapsSuite) TestRefreshMany1(c *check.C) {
   615  	refreshSnapDecls := false
   616  	defer daemon.MockAssertstateRefreshSnapDeclarations(func(s *state.State, userID int) error {
   617  		refreshSnapDecls = true
   618  		return nil
   619  	})()
   620  
   621  	defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) {
   622  		c.Check(names, check.HasLen, 1)
   623  		t := s.NewTask("fake-refresh-1", "Refreshing one")
   624  		return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
   625  	})()
   626  
   627  	d := s.daemon(c)
   628  	inst := &daemon.SnapInstruction{Action: "refresh", Snaps: []string{"foo"}}
   629  	st := d.Overlord().State()
   630  	st.Lock()
   631  	res, err := inst.DispatchForMany()(inst, st)
   632  	st.Unlock()
   633  	c.Assert(err, check.IsNil)
   634  	c.Check(res.Summary, check.Equals, `Refresh snap "foo"`)
   635  	c.Check(res.Affected, check.DeepEquals, inst.Snaps)
   636  	c.Check(refreshSnapDecls, check.Equals, true)
   637  }
   638  
   639  func (s *snapsSuite) TestInstallMany(c *check.C) {
   640  	defer daemon.MockSnapstateInstallMany(func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) {
   641  		c.Check(names, check.HasLen, 2)
   642  		t := s.NewTask("fake-install-2", "Install two")
   643  		return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
   644  	})()
   645  
   646  	d := s.daemon(c)
   647  	inst := &daemon.SnapInstruction{Action: "install", Snaps: []string{"foo", "bar"}}
   648  	st := d.Overlord().State()
   649  	st.Lock()
   650  	res, err := inst.DispatchForMany()(inst, st)
   651  	st.Unlock()
   652  	c.Assert(err, check.IsNil)
   653  	c.Check(res.Summary, check.Equals, `Install snaps "foo", "bar"`)
   654  	c.Check(res.Affected, check.DeepEquals, inst.Snaps)
   655  }
   656  
   657  func (s *snapsSuite) TestInstallManyEmptyName(c *check.C) {
   658  	defer daemon.MockSnapstateInstallMany(func(_ *state.State, _ []string, _ int) ([]string, []*state.TaskSet, error) {
   659  		return nil, nil, errors.New("should not be called")
   660  	})()
   661  	d := s.daemon(c)
   662  	inst := &daemon.SnapInstruction{Action: "install", Snaps: []string{"", "bar"}}
   663  	st := d.Overlord().State()
   664  	st.Lock()
   665  	res, err := inst.DispatchForMany()(inst, st)
   666  	st.Unlock()
   667  	c.Assert(res, check.IsNil)
   668  	c.Assert(err, check.ErrorMatches, "cannot install snap with empty name")
   669  }
   670  
   671  func (s *snapsSuite) TestRemoveMany(c *check.C) {
   672  	defer daemon.MockSnapstateRemoveMany(func(s *state.State, names []string) ([]string, []*state.TaskSet, error) {
   673  		c.Check(names, check.HasLen, 2)
   674  		t := s.NewTask("fake-remove-2", "Remove two")
   675  		return names, []*state.TaskSet{state.NewTaskSet(t)}, nil
   676  	})()
   677  
   678  	d := s.daemon(c)
   679  	inst := &daemon.SnapInstruction{Action: "remove", Snaps: []string{"foo", "bar"}}
   680  	st := d.Overlord().State()
   681  	st.Lock()
   682  	res, err := inst.DispatchForMany()(inst, st)
   683  	st.Unlock()
   684  	c.Assert(err, check.IsNil)
   685  	c.Check(res.Summary, check.Equals, `Remove snaps "foo", "bar"`)
   686  	c.Check(res.Affected, check.DeepEquals, inst.Snaps)
   687  }