github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/store/store_action_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package store_test
    21  
    22  import (
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"net/url"
    30  	"os"
    31  	"strings"
    32  	"time"
    33  
    34  	. "gopkg.in/check.v1"
    35  
    36  	"github.com/snapcore/snapd/arch"
    37  	"github.com/snapcore/snapd/release"
    38  	"github.com/snapcore/snapd/snap"
    39  	"github.com/snapcore/snapd/snap/channel"
    40  	"github.com/snapcore/snapd/snap/snaptest"
    41  	"github.com/snapcore/snapd/store"
    42  	"github.com/snapcore/snapd/testutil"
    43  )
    44  
    45  type storeActionSuite struct {
    46  	baseStoreSuite
    47  
    48  	mockXDelta *testutil.MockCmd
    49  }
    50  
    51  var _ = Suite(&storeActionSuite{})
    52  
    53  func (s *storeActionSuite) SetUpTest(c *C) {
    54  	s.baseStoreSuite.SetUpTest(c)
    55  
    56  	s.mockXDelta = testutil.MockCommand(c, "xdelta3", "")
    57  	s.AddCleanup(s.mockXDelta.Restore)
    58  }
    59  
    60  var (
    61  	helloRefreshedDateStr = "2018-02-27T11:00:00Z"
    62  	helloRefreshedDate    time.Time
    63  )
    64  
    65  func init() {
    66  	t, err := time.Parse(time.RFC3339, helloRefreshedDateStr)
    67  	if err != nil {
    68  		panic(err)
    69  	}
    70  	helloRefreshedDate = t
    71  }
    72  
    73  const helloCohortKey = "this is a very short cohort key, as cohort keys go, because those are *long*"
    74  
    75  func (s *storeActionSuite) TestSnapAction(c *C) {
    76  	restore := release.MockOnClassic(false)
    77  	defer restore()
    78  
    79  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    80  		assertRequest(c, r, "POST", snapActionPath)
    81  		// check device authorization is set, implicitly checking doRequest was used
    82  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
    83  
    84  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
    85  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "")
    86  
    87  		// no store ID by default
    88  		storeID := r.Header.Get("Snap-Device-Store")
    89  		c.Check(storeID, Equals, "")
    90  
    91  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
    92  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
    93  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
    94  
    95  		jsonReq, err := ioutil.ReadAll(r.Body)
    96  		c.Assert(err, IsNil)
    97  		var req struct {
    98  			Context []map[string]interface{} `json:"context"`
    99  			Fields  []string                 `json:"fields"`
   100  			Actions []map[string]interface{} `json:"actions"`
   101  		}
   102  
   103  		err = json.Unmarshal(jsonReq, &req)
   104  		c.Assert(err, IsNil)
   105  
   106  		c.Check(req.Fields, DeepEquals, store.SnapActionFields)
   107  
   108  		c.Assert(req.Context, HasLen, 1)
   109  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   110  			"snap-id":          helloWorldSnapID,
   111  			"instance-key":     helloWorldSnapID,
   112  			"revision":         float64(1),
   113  			"tracking-channel": "beta",
   114  			"refreshed-date":   helloRefreshedDateStr,
   115  			"epoch":            iZeroEpoch,
   116  		})
   117  		c.Assert(req.Actions, HasLen, 1)
   118  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   119  			"action":       "refresh",
   120  			"instance-key": helloWorldSnapID,
   121  			"snap-id":      helloWorldSnapID,
   122  			"cohort-key":   helloCohortKey,
   123  		})
   124  
   125  		io.WriteString(w, `{
   126    "results": [{
   127       "result": "refresh",
   128       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   129       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   130       "name": "hello-world",
   131       "snap": {
   132         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   133         "name": "hello-world",
   134         "revision": 26,
   135         "version": "6.1",
   136         "epoch": {"read": [0], "write": [0]},
   137         "publisher": {
   138            "id": "canonical",
   139            "username": "canonical",
   140            "display-name": "Canonical"
   141         }
   142       }
   143    }]
   144  }`)
   145  	}))
   146  
   147  	c.Assert(mockServer, NotNil)
   148  	defer mockServer.Close()
   149  
   150  	mockServerURL, _ := url.Parse(mockServer.URL)
   151  	cfg := store.Config{
   152  		StoreBaseURL: mockServerURL,
   153  	}
   154  	dauthCtx := &testDauthContext{c: c, device: s.device}
   155  	sto := store.New(&cfg, dauthCtx)
   156  
   157  	results, aresults, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   158  		{
   159  			InstanceName:    "hello-world",
   160  			SnapID:          helloWorldSnapID,
   161  			TrackingChannel: "beta",
   162  			Revision:        snap.R(1),
   163  			RefreshedDate:   helloRefreshedDate,
   164  		},
   165  	}, []*store.SnapAction{
   166  		{
   167  			Action:       "refresh",
   168  			SnapID:       helloWorldSnapID,
   169  			InstanceName: "hello-world",
   170  			CohortKey:    helloCohortKey,
   171  		},
   172  	}, nil, nil, nil)
   173  	c.Assert(err, IsNil)
   174  	c.Assert(aresults, HasLen, 0)
   175  	c.Assert(results, HasLen, 1)
   176  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   177  	c.Assert(results[0].Revision, Equals, snap.R(26))
   178  	c.Assert(results[0].Version, Equals, "6.1")
   179  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
   180  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
   181  	c.Assert(results[0].Deltas, HasLen, 0)
   182  	c.Assert(results[0].Epoch, DeepEquals, snap.E("0"))
   183  }
   184  
   185  func (s *storeActionSuite) TestSnapActionNonZeroEpochAndEpochBump(c *C) {
   186  	restore := release.MockOnClassic(false)
   187  	defer restore()
   188  
   189  	numReqs := 0
   190  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   191  		numReqs++
   192  		assertRequest(c, r, "POST", snapActionPath)
   193  		// check device authorization is set, implicitly checking doRequest was used
   194  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   195  
   196  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
   197  
   198  		// no store ID by default
   199  		storeID := r.Header.Get("Snap-Device-Store")
   200  		c.Check(storeID, Equals, "")
   201  
   202  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
   203  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
   204  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
   205  
   206  		jsonReq, err := ioutil.ReadAll(r.Body)
   207  		c.Assert(err, IsNil)
   208  		var req struct {
   209  			Context []map[string]interface{} `json:"context"`
   210  			Fields  []string                 `json:"fields"`
   211  			Actions []map[string]interface{} `json:"actions"`
   212  		}
   213  
   214  		err = json.Unmarshal(jsonReq, &req)
   215  		c.Assert(err, IsNil)
   216  
   217  		c.Check(req.Fields, DeepEquals, store.SnapActionFields)
   218  
   219  		c.Assert(req.Context, HasLen, 1)
   220  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   221  			"snap-id":          helloWorldSnapID,
   222  			"instance-key":     helloWorldSnapID,
   223  			"revision":         float64(1),
   224  			"tracking-channel": "beta",
   225  			"refreshed-date":   helloRefreshedDateStr,
   226  			"epoch":            iFiveStarEpoch,
   227  		})
   228  		c.Assert(req.Actions, HasLen, 1)
   229  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   230  			"action":       "refresh",
   231  			"instance-key": helloWorldSnapID,
   232  			"snap-id":      helloWorldSnapID,
   233  		})
   234  
   235  		io.WriteString(w, `{
   236    "results": [{
   237       "result": "refresh",
   238       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   239       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   240       "name": "hello-world",
   241       "snap": {
   242         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   243         "name": "hello-world",
   244         "revision": 26,
   245         "version": "6.1",
   246         "epoch": {"read": [5, 6], "write": [6]},
   247         "publisher": {
   248            "id": "canonical",
   249            "username": "canonical",
   250            "display-name": "Canonical"
   251         }
   252       }
   253    }]
   254  }`)
   255  	}))
   256  
   257  	c.Assert(mockServer, NotNil)
   258  	defer mockServer.Close()
   259  
   260  	mockServerURL, _ := url.Parse(mockServer.URL)
   261  	cfg := store.Config{
   262  		StoreBaseURL: mockServerURL,
   263  	}
   264  	dauthCtx := &testDauthContext{c: c, device: s.device}
   265  	sto := store.New(&cfg, dauthCtx)
   266  
   267  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   268  		{
   269  			InstanceName:    "hello-world",
   270  			SnapID:          helloWorldSnapID,
   271  			TrackingChannel: "beta",
   272  			Revision:        snap.R(1),
   273  			RefreshedDate:   helloRefreshedDate,
   274  			Epoch:           snap.E("5*"),
   275  		},
   276  	}, []*store.SnapAction{
   277  		{
   278  			Action:       "refresh",
   279  			SnapID:       helloWorldSnapID,
   280  			InstanceName: "hello-world",
   281  		},
   282  	}, nil, nil, nil)
   283  	c.Assert(err, IsNil)
   284  	c.Assert(results, HasLen, 1)
   285  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   286  	c.Assert(results[0].Revision, Equals, snap.R(26))
   287  	c.Assert(results[0].Version, Equals, "6.1")
   288  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
   289  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
   290  	c.Assert(results[0].Deltas, HasLen, 0)
   291  	c.Assert(results[0].Epoch, DeepEquals, snap.E("6*"))
   292  
   293  	c.Assert(numReqs, Equals, 1) // should be >1 soon :-)
   294  }
   295  
   296  func (s *storeActionSuite) TestSnapActionNoResults(c *C) {
   297  	restore := release.MockOnClassic(false)
   298  	defer restore()
   299  
   300  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   301  		assertRequest(c, r, "POST", snapActionPath)
   302  		// check device authorization is set, implicitly checking doRequest was used
   303  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   304  
   305  		jsonReq, err := ioutil.ReadAll(r.Body)
   306  		c.Assert(err, IsNil)
   307  		var req struct {
   308  			Context []map[string]interface{} `json:"context"`
   309  			Actions []map[string]interface{} `json:"actions"`
   310  		}
   311  
   312  		err = json.Unmarshal(jsonReq, &req)
   313  		c.Assert(err, IsNil)
   314  
   315  		c.Assert(req.Context, HasLen, 1)
   316  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   317  			"snap-id":          helloWorldSnapID,
   318  			"instance-key":     helloWorldSnapID,
   319  			"revision":         float64(1),
   320  			"tracking-channel": "beta",
   321  			"refreshed-date":   helloRefreshedDateStr,
   322  			"epoch":            iZeroEpoch,
   323  		})
   324  		c.Assert(req.Actions, HasLen, 0)
   325  		io.WriteString(w, `{
   326    "results": []
   327  }`)
   328  	}))
   329  
   330  	c.Assert(mockServer, NotNil)
   331  	defer mockServer.Close()
   332  
   333  	mockServerURL, _ := url.Parse(mockServer.URL)
   334  	cfg := store.Config{
   335  		StoreBaseURL: mockServerURL,
   336  	}
   337  	dauthCtx := &testDauthContext{c: c, device: s.device}
   338  	sto := store.New(&cfg, dauthCtx)
   339  
   340  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   341  		{
   342  			InstanceName:    "hello-world",
   343  			SnapID:          helloWorldSnapID,
   344  			TrackingChannel: "beta",
   345  			Revision:        snap.R(1),
   346  			RefreshedDate:   helloRefreshedDate,
   347  		},
   348  	}, nil, nil, nil, nil)
   349  	c.Check(results, HasLen, 0)
   350  	c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true})
   351  
   352  	// local no-op
   353  	results, _, err = sto.SnapAction(s.ctx, nil, nil, nil, nil, nil)
   354  	c.Check(results, HasLen, 0)
   355  	c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true})
   356  
   357  	c.Check(err.Error(), Equals, "no install/refresh information results from the store")
   358  }
   359  
   360  func (s *storeActionSuite) TestSnapActionRefreshedDateIsOptional(c *C) {
   361  	restore := release.MockOnClassic(false)
   362  	defer restore()
   363  
   364  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   365  		assertRequest(c, r, "POST", snapActionPath)
   366  		// check device authorization is set, implicitly checking doRequest was used
   367  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   368  
   369  		jsonReq, err := ioutil.ReadAll(r.Body)
   370  		c.Assert(err, IsNil)
   371  		var req struct {
   372  			Context []map[string]interface{} `json:"context"`
   373  			Actions []map[string]interface{} `json:"actions"`
   374  		}
   375  
   376  		err = json.Unmarshal(jsonReq, &req)
   377  		c.Assert(err, IsNil)
   378  
   379  		c.Assert(req.Context, HasLen, 1)
   380  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   381  			"snap-id":      helloWorldSnapID,
   382  			"instance-key": helloWorldSnapID,
   383  
   384  			"revision":         float64(1),
   385  			"tracking-channel": "beta",
   386  			"epoch":            iZeroEpoch,
   387  		})
   388  		c.Assert(req.Actions, HasLen, 0)
   389  		io.WriteString(w, `{
   390    "results": []
   391  }`)
   392  	}))
   393  
   394  	c.Assert(mockServer, NotNil)
   395  	defer mockServer.Close()
   396  
   397  	mockServerURL, _ := url.Parse(mockServer.URL)
   398  	cfg := store.Config{
   399  		StoreBaseURL: mockServerURL,
   400  	}
   401  	dauthCtx := &testDauthContext{c: c, device: s.device}
   402  	sto := store.New(&cfg, dauthCtx)
   403  
   404  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   405  		{
   406  			InstanceName:    "hello-world",
   407  			SnapID:          helloWorldSnapID,
   408  			TrackingChannel: "beta",
   409  			Revision:        snap.R(1),
   410  		},
   411  	}, nil, nil, nil, nil)
   412  	c.Check(results, HasLen, 0)
   413  	c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true})
   414  }
   415  
   416  func (s *storeActionSuite) TestSnapActionSkipBlocked(c *C) {
   417  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   418  		assertRequest(c, r, "POST", snapActionPath)
   419  		// check device authorization is set, implicitly checking doRequest was used
   420  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   421  
   422  		jsonReq, err := ioutil.ReadAll(r.Body)
   423  		c.Assert(err, IsNil)
   424  		var req struct {
   425  			Context []map[string]interface{} `json:"context"`
   426  			Actions []map[string]interface{} `json:"actions"`
   427  		}
   428  
   429  		err = json.Unmarshal(jsonReq, &req)
   430  		c.Assert(err, IsNil)
   431  
   432  		c.Assert(req.Context, HasLen, 1)
   433  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   434  			"snap-id":          helloWorldSnapID,
   435  			"instance-key":     helloWorldSnapID,
   436  			"revision":         float64(1),
   437  			"tracking-channel": "stable",
   438  			"refreshed-date":   helloRefreshedDateStr,
   439  			"epoch":            iZeroEpoch,
   440  		})
   441  		c.Assert(req.Actions, HasLen, 1)
   442  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   443  			"action":       "refresh",
   444  			"instance-key": helloWorldSnapID,
   445  			"snap-id":      helloWorldSnapID,
   446  			"channel":      "stable",
   447  		})
   448  
   449  		io.WriteString(w, `{
   450    "results": [{
   451       "result": "refresh",
   452       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   453       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   454       "name": "hello-world",
   455       "snap": {
   456         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   457         "name": "hello-world",
   458         "revision": 26,
   459         "version": "6.1",
   460         "publisher": {
   461            "id": "canonical",
   462            "username": "canonical",
   463            "display-name": "Canonical"
   464         }
   465       }
   466    }]
   467  }`)
   468  	}))
   469  
   470  	c.Assert(mockServer, NotNil)
   471  	defer mockServer.Close()
   472  
   473  	mockServerURL, _ := url.Parse(mockServer.URL)
   474  	cfg := store.Config{
   475  		StoreBaseURL: mockServerURL,
   476  	}
   477  	dauthCtx := &testDauthContext{c: c, device: s.device}
   478  	sto := store.New(&cfg, dauthCtx)
   479  
   480  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   481  		{
   482  			InstanceName:    "hello-world",
   483  			SnapID:          helloWorldSnapID,
   484  			TrackingChannel: "stable",
   485  			Revision:        snap.R(1),
   486  			RefreshedDate:   helloRefreshedDate,
   487  			Block:           []snap.Revision{snap.R(26)},
   488  		},
   489  	}, []*store.SnapAction{
   490  		{
   491  			Action:       "refresh",
   492  			SnapID:       helloWorldSnapID,
   493  			InstanceName: "hello-world",
   494  			Channel:      "stable",
   495  		},
   496  	}, nil, nil, nil)
   497  	c.Assert(results, HasLen, 0)
   498  	c.Check(err, DeepEquals, &store.SnapActionError{
   499  		Refresh: map[string]error{
   500  			"hello-world": store.ErrNoUpdateAvailable,
   501  		},
   502  	})
   503  }
   504  
   505  func (s *storeActionSuite) TestSnapActionSkipCurrent(c *C) {
   506  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   507  		assertRequest(c, r, "POST", snapActionPath)
   508  		// check device authorization is set, implicitly checking doRequest was used
   509  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   510  
   511  		jsonReq, err := ioutil.ReadAll(r.Body)
   512  		c.Assert(err, IsNil)
   513  		var req struct {
   514  			Context []map[string]interface{} `json:"context"`
   515  			Actions []map[string]interface{} `json:"actions"`
   516  		}
   517  
   518  		err = json.Unmarshal(jsonReq, &req)
   519  		c.Assert(err, IsNil)
   520  
   521  		c.Assert(req.Context, HasLen, 1)
   522  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   523  			"snap-id":          helloWorldSnapID,
   524  			"instance-key":     helloWorldSnapID,
   525  			"revision":         float64(26),
   526  			"tracking-channel": "stable",
   527  			"refreshed-date":   helloRefreshedDateStr,
   528  			"epoch":            iZeroEpoch,
   529  		})
   530  		c.Assert(req.Actions, HasLen, 1)
   531  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   532  			"action":       "refresh",
   533  			"instance-key": helloWorldSnapID,
   534  			"snap-id":      helloWorldSnapID,
   535  			"channel":      "stable",
   536  		})
   537  
   538  		io.WriteString(w, `{
   539    "results": [{
   540       "result": "refresh",
   541       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   542       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   543       "name": "hello-world",
   544       "snap": {
   545         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   546         "name": "hello-world",
   547         "revision": 26,
   548         "version": "6.1",
   549         "publisher": {
   550            "id": "canonical",
   551            "username": "canonical",
   552            "display-name": "Canonical"
   553         }
   554       }
   555    }]
   556  }`)
   557  	}))
   558  
   559  	c.Assert(mockServer, NotNil)
   560  	defer mockServer.Close()
   561  
   562  	mockServerURL, _ := url.Parse(mockServer.URL)
   563  	cfg := store.Config{
   564  		StoreBaseURL: mockServerURL,
   565  	}
   566  	dauthCtx := &testDauthContext{c: c, device: s.device}
   567  	sto := store.New(&cfg, dauthCtx)
   568  
   569  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   570  		{
   571  			InstanceName:    "hello-world",
   572  			SnapID:          helloWorldSnapID,
   573  			TrackingChannel: "stable",
   574  			Revision:        snap.R(26),
   575  			RefreshedDate:   helloRefreshedDate,
   576  		},
   577  	}, []*store.SnapAction{
   578  		{
   579  			Action:       "refresh",
   580  			SnapID:       helloWorldSnapID,
   581  			InstanceName: "hello-world",
   582  			Channel:      "stable",
   583  		},
   584  	}, nil, nil, nil)
   585  	c.Assert(results, HasLen, 0)
   586  	c.Check(err, DeepEquals, &store.SnapActionError{
   587  		Refresh: map[string]error{
   588  			"hello-world": store.ErrNoUpdateAvailable,
   589  		},
   590  	})
   591  }
   592  
   593  func (s *storeActionSuite) TestSnapActionRetryOnEOF(c *C) {
   594  	n := 0
   595  	var mockServer *httptest.Server
   596  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   597  		assertRequest(c, r, "POST", snapActionPath)
   598  		n++
   599  		if n < 4 {
   600  			io.WriteString(w, "{")
   601  			mockServer.CloseClientConnections()
   602  			return
   603  		}
   604  
   605  		var req struct {
   606  			Context []map[string]interface{} `json:"context"`
   607  			Actions []map[string]interface{} `json:"actions"`
   608  		}
   609  
   610  		err := json.NewDecoder(r.Body).Decode(&req)
   611  		c.Assert(err, IsNil)
   612  		c.Assert(req.Context, HasLen, 1)
   613  		c.Assert(req.Actions, HasLen, 1)
   614  		io.WriteString(w, `{
   615    "results": [{
   616       "result": "refresh",
   617       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   618       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   619       "name": "hello-world",
   620       "snap": {
   621         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   622         "name": "hello-world",
   623         "revision": 26,
   624         "version": "6.1",
   625         "publisher": {
   626            "id": "canonical",
   627            "username": "canonical",
   628            "display-name": "Canonical"
   629         }
   630       }
   631    }]
   632  }`)
   633  	}))
   634  
   635  	c.Assert(mockServer, NotNil)
   636  	defer mockServer.Close()
   637  
   638  	mockServerURL, _ := url.Parse(mockServer.URL)
   639  	cfg := store.Config{
   640  		StoreBaseURL: mockServerURL,
   641  	}
   642  	dauthCtx := &testDauthContext{c: c, device: s.device}
   643  	sto := store.New(&cfg, dauthCtx)
   644  
   645  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   646  		{
   647  			InstanceName:    "hello-world",
   648  			SnapID:          helloWorldSnapID,
   649  			TrackingChannel: "stable",
   650  			Revision:        snap.R(1),
   651  		},
   652  	}, []*store.SnapAction{
   653  		{
   654  			Action:       "refresh",
   655  			SnapID:       helloWorldSnapID,
   656  			InstanceName: "hello-world",
   657  			Channel:      "stable",
   658  		},
   659  	}, nil, nil, nil)
   660  	c.Assert(err, IsNil)
   661  	c.Assert(n, Equals, 4)
   662  	c.Assert(results, HasLen, 1)
   663  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   664  }
   665  
   666  func (s *storeActionSuite) TestSnapActionIgnoreValidation(c *C) {
   667  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   668  		assertRequest(c, r, "POST", snapActionPath)
   669  		// check device authorization is set, implicitly checking doRequest was used
   670  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   671  
   672  		jsonReq, err := ioutil.ReadAll(r.Body)
   673  		c.Assert(err, IsNil)
   674  		var req struct {
   675  			Context []map[string]interface{} `json:"context"`
   676  			Actions []map[string]interface{} `json:"actions"`
   677  		}
   678  
   679  		err = json.Unmarshal(jsonReq, &req)
   680  		c.Assert(err, IsNil)
   681  
   682  		c.Assert(req.Context, HasLen, 1)
   683  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   684  			"snap-id":           helloWorldSnapID,
   685  			"instance-key":      helloWorldSnapID,
   686  			"revision":          float64(1),
   687  			"tracking-channel":  "stable",
   688  			"refreshed-date":    helloRefreshedDateStr,
   689  			"ignore-validation": true,
   690  			"epoch":             iZeroEpoch,
   691  		})
   692  		c.Assert(req.Actions, HasLen, 1)
   693  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   694  			"action":            "refresh",
   695  			"instance-key":      helloWorldSnapID,
   696  			"snap-id":           helloWorldSnapID,
   697  			"channel":           "stable",
   698  			"ignore-validation": false,
   699  		})
   700  
   701  		io.WriteString(w, `{
   702    "results": [{
   703       "result": "refresh",
   704       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   705       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   706       "name": "hello-world",
   707       "snap": {
   708         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   709         "name": "hello-world",
   710         "revision": 26,
   711         "version": "6.1",
   712         "publisher": {
   713            "id": "canonical",
   714            "username": "canonical",
   715            "display-name": "Canonical"
   716         }
   717       }
   718    }]
   719  }`)
   720  	}))
   721  
   722  	c.Assert(mockServer, NotNil)
   723  	defer mockServer.Close()
   724  
   725  	mockServerURL, _ := url.Parse(mockServer.URL)
   726  	cfg := store.Config{
   727  		StoreBaseURL: mockServerURL,
   728  	}
   729  	dauthCtx := &testDauthContext{c: c, device: s.device}
   730  	sto := store.New(&cfg, dauthCtx)
   731  
   732  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   733  		{
   734  			InstanceName:     "hello-world",
   735  			SnapID:           helloWorldSnapID,
   736  			TrackingChannel:  "stable",
   737  			Revision:         snap.R(1),
   738  			RefreshedDate:    helloRefreshedDate,
   739  			IgnoreValidation: true,
   740  		},
   741  	}, []*store.SnapAction{
   742  		{
   743  			Action:       "refresh",
   744  			SnapID:       helloWorldSnapID,
   745  			InstanceName: "hello-world",
   746  			Channel:      "stable",
   747  			Flags:        store.SnapActionEnforceValidation,
   748  		},
   749  	}, nil, nil, nil)
   750  	c.Assert(err, IsNil)
   751  	c.Assert(results, HasLen, 1)
   752  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   753  	c.Assert(results[0].Revision, Equals, snap.R(26))
   754  }
   755  
   756  func (s *storeActionSuite) TestSnapActionAutoRefresh(c *C) {
   757  	// the bare TestSnapAction does more SnapAction checks; look there
   758  	// this one mostly just checks the refresh-reason header
   759  
   760  	restore := release.MockOnClassic(false)
   761  	defer restore()
   762  
   763  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   764  		assertRequest(c, r, "POST", snapActionPath)
   765  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled")
   766  
   767  		io.WriteString(w, `{
   768    "results": [{
   769       "result": "refresh",
   770       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   771       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   772       "name": "hello-world",
   773       "snap": {
   774         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   775         "name": "hello-world",
   776         "revision": 26,
   777         "version": "6.1",
   778         "epoch": {"read": [0], "write": [0]},
   779         "publisher": {
   780            "id": "canonical",
   781            "username": "canonical",
   782            "display-name": "Canonical"
   783         }
   784       }
   785    }]
   786  }`)
   787  	}))
   788  
   789  	c.Assert(mockServer, NotNil)
   790  	defer mockServer.Close()
   791  
   792  	mockServerURL, _ := url.Parse(mockServer.URL)
   793  	cfg := store.Config{
   794  		StoreBaseURL: mockServerURL,
   795  	}
   796  	dauthCtx := &testDauthContext{c: c, device: s.device}
   797  	sto := store.New(&cfg, dauthCtx)
   798  
   799  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   800  		{
   801  			InstanceName:    "hello-world",
   802  			SnapID:          helloWorldSnapID,
   803  			TrackingChannel: "beta",
   804  			Revision:        snap.R(1),
   805  			RefreshedDate:   helloRefreshedDate,
   806  		},
   807  	}, []*store.SnapAction{
   808  		{
   809  			Action:       "refresh",
   810  			SnapID:       helloWorldSnapID,
   811  			InstanceName: "hello-world",
   812  		},
   813  	}, nil, nil, &store.RefreshOptions{IsAutoRefresh: true})
   814  	c.Assert(err, IsNil)
   815  	c.Assert(results, HasLen, 1)
   816  }
   817  
   818  func (s *storeActionSuite) TestInstallFallbackChannelIsStable(c *C) {
   819  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   820  		assertRequest(c, r, "POST", snapActionPath)
   821  		// check device authorization is set, implicitly checking doRequest was used
   822  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   823  
   824  		jsonReq, err := ioutil.ReadAll(r.Body)
   825  		c.Assert(err, IsNil)
   826  		var req struct {
   827  			Context []map[string]interface{} `json:"context"`
   828  			Actions []map[string]interface{} `json:"actions"`
   829  		}
   830  
   831  		err = json.Unmarshal(jsonReq, &req)
   832  		c.Assert(err, IsNil)
   833  
   834  		c.Assert(req.Context, HasLen, 1)
   835  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   836  			"snap-id":          helloWorldSnapID,
   837  			"instance-key":     helloWorldSnapID,
   838  			"revision":         float64(1),
   839  			"tracking-channel": "stable",
   840  			"refreshed-date":   helloRefreshedDateStr,
   841  			"epoch":            iZeroEpoch,
   842  		})
   843  		c.Assert(req.Actions, HasLen, 1)
   844  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   845  			"action":       "refresh",
   846  			"instance-key": helloWorldSnapID,
   847  			"snap-id":      helloWorldSnapID,
   848  		})
   849  
   850  		io.WriteString(w, `{
   851    "results": [{
   852       "result": "refresh",
   853       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   854       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   855       "name": "hello-world",
   856       "snap": {
   857         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   858         "name": "hello-world",
   859         "revision": 26,
   860         "version": "6.1",
   861         "publisher": {
   862            "id": "canonical",
   863            "username": "canonical",
   864            "display-name": "Canonical"
   865         }
   866       }
   867    }]
   868  }`)
   869  	}))
   870  
   871  	c.Assert(mockServer, NotNil)
   872  	defer mockServer.Close()
   873  
   874  	mockServerURL, _ := url.Parse(mockServer.URL)
   875  	cfg := store.Config{
   876  		StoreBaseURL: mockServerURL,
   877  	}
   878  	dauthCtx := &testDauthContext{c: c, device: s.device}
   879  	sto := store.New(&cfg, dauthCtx)
   880  
   881  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   882  		{
   883  			InstanceName:  "hello-world",
   884  			SnapID:        helloWorldSnapID,
   885  			RefreshedDate: helloRefreshedDate,
   886  			Revision:      snap.R(1),
   887  		},
   888  	}, []*store.SnapAction{
   889  		{
   890  			Action:       "refresh",
   891  			SnapID:       helloWorldSnapID,
   892  			InstanceName: "hello-world",
   893  		},
   894  	}, nil, nil, nil)
   895  	c.Assert(err, IsNil)
   896  	c.Assert(results, HasLen, 1)
   897  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   898  	c.Assert(results[0].Revision, Equals, snap.R(26))
   899  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
   900  }
   901  
   902  func (s *storeActionSuite) TestSnapActionNonDefaultsHeaders(c *C) {
   903  	restore := release.MockOnClassic(true)
   904  	defer restore()
   905  
   906  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   907  		assertRequest(c, r, "POST", snapActionPath)
   908  		// check device authorization is set, implicitly checking doRequest was used
   909  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   910  
   911  		storeID := r.Header.Get("Snap-Device-Store")
   912  		c.Check(storeID, Equals, "foo")
   913  
   914  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, "21")
   915  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, "archXYZ")
   916  		c.Check(r.Header.Get("Snap-Classic"), Equals, "true")
   917  
   918  		jsonReq, err := ioutil.ReadAll(r.Body)
   919  		c.Assert(err, IsNil)
   920  		var req struct {
   921  			Context []map[string]interface{} `json:"context"`
   922  			Actions []map[string]interface{} `json:"actions"`
   923  		}
   924  
   925  		err = json.Unmarshal(jsonReq, &req)
   926  		c.Assert(err, IsNil)
   927  
   928  		c.Assert(req.Context, HasLen, 1)
   929  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   930  			"snap-id":          helloWorldSnapID,
   931  			"instance-key":     helloWorldSnapID,
   932  			"revision":         float64(1),
   933  			"tracking-channel": "beta",
   934  			"refreshed-date":   helloRefreshedDateStr,
   935  			"epoch":            iZeroEpoch,
   936  		})
   937  		c.Assert(req.Actions, HasLen, 1)
   938  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   939  			"action":       "refresh",
   940  			"instance-key": helloWorldSnapID,
   941  			"snap-id":      helloWorldSnapID,
   942  		})
   943  
   944  		io.WriteString(w, `{
   945    "results": [{
   946       "result": "refresh",
   947       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   948       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   949       "name": "hello-world",
   950       "snap": {
   951         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   952         "name": "hello-world",
   953         "revision": 26,
   954         "version": "6.1",
   955         "publisher": {
   956            "id": "canonical",
   957            "username": "canonical",
   958            "display-name": "Canonical"
   959         }
   960       }
   961    }]
   962  }`)
   963  	}))
   964  
   965  	c.Assert(mockServer, NotNil)
   966  	defer mockServer.Close()
   967  
   968  	mockServerURL, _ := url.Parse(mockServer.URL)
   969  	cfg := store.DefaultConfig()
   970  	cfg.StoreBaseURL = mockServerURL
   971  	cfg.Series = "21"
   972  	cfg.Architecture = "archXYZ"
   973  	cfg.StoreID = "foo"
   974  	dauthCtx := &testDauthContext{c: c, device: s.device}
   975  	sto := store.New(cfg, dauthCtx)
   976  
   977  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   978  		{
   979  			InstanceName:    "hello-world",
   980  			SnapID:          helloWorldSnapID,
   981  			TrackingChannel: "beta",
   982  			RefreshedDate:   helloRefreshedDate,
   983  			Revision:        snap.R(1),
   984  		},
   985  	}, []*store.SnapAction{
   986  		{
   987  			Action:       "refresh",
   988  			SnapID:       helloWorldSnapID,
   989  			InstanceName: "hello-world",
   990  		},
   991  	}, nil, nil, nil)
   992  	c.Assert(err, IsNil)
   993  	c.Assert(results, HasLen, 1)
   994  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   995  	c.Assert(results[0].Revision, Equals, snap.R(26))
   996  	c.Assert(results[0].Version, Equals, "6.1")
   997  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
   998  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
   999  	c.Assert(results[0].Deltas, HasLen, 0)
  1000  }
  1001  
  1002  func (s *storeActionSuite) TestSnapActionWithDeltas(c *C) {
  1003  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
  1004  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
  1005  	c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
  1006  
  1007  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1008  		assertRequest(c, r, "POST", snapActionPath)
  1009  		// check device authorization is set, implicitly checking doRequest was used
  1010  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1011  
  1012  		c.Check(r.Header.Get("Snap-Accept-Delta-Format"), Equals, "xdelta3")
  1013  		jsonReq, err := ioutil.ReadAll(r.Body)
  1014  		c.Assert(err, IsNil)
  1015  		var req struct {
  1016  			Context []map[string]interface{} `json:"context"`
  1017  			Actions []map[string]interface{} `json:"actions"`
  1018  		}
  1019  
  1020  		err = json.Unmarshal(jsonReq, &req)
  1021  		c.Assert(err, IsNil)
  1022  
  1023  		c.Assert(req.Context, HasLen, 1)
  1024  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1025  			"snap-id":          helloWorldSnapID,
  1026  			"instance-key":     helloWorldSnapID,
  1027  			"revision":         float64(1),
  1028  			"tracking-channel": "beta",
  1029  			"refreshed-date":   helloRefreshedDateStr,
  1030  			"epoch":            iZeroEpoch,
  1031  		})
  1032  		c.Assert(req.Actions, HasLen, 1)
  1033  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1034  			"action":       "refresh",
  1035  			"instance-key": helloWorldSnapID,
  1036  			"snap-id":      helloWorldSnapID,
  1037  		})
  1038  
  1039  		io.WriteString(w, `{
  1040    "results": [{
  1041       "result": "refresh",
  1042       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1043       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1044       "name": "hello-world",
  1045       "snap": {
  1046         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1047         "name": "hello-world",
  1048         "revision": 26,
  1049         "version": "6.1",
  1050         "publisher": {
  1051            "id": "canonical",
  1052            "username": "canonical",
  1053            "display-name": "Canonical"
  1054         }
  1055       }
  1056    }]
  1057  }`)
  1058  	}))
  1059  
  1060  	c.Assert(mockServer, NotNil)
  1061  	defer mockServer.Close()
  1062  
  1063  	mockServerURL, _ := url.Parse(mockServer.URL)
  1064  	cfg := store.Config{
  1065  		StoreBaseURL: mockServerURL,
  1066  	}
  1067  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1068  	sto := store.New(&cfg, dauthCtx)
  1069  
  1070  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1071  		{
  1072  			InstanceName:    "hello-world",
  1073  			SnapID:          helloWorldSnapID,
  1074  			TrackingChannel: "beta",
  1075  			Revision:        snap.R(1),
  1076  			RefreshedDate:   helloRefreshedDate,
  1077  		},
  1078  	}, []*store.SnapAction{
  1079  		{
  1080  			Action:       "refresh",
  1081  			SnapID:       helloWorldSnapID,
  1082  			InstanceName: "hello-world",
  1083  		},
  1084  	}, nil, nil, nil)
  1085  	c.Assert(err, IsNil)
  1086  	c.Assert(results, HasLen, 1)
  1087  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1088  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1089  }
  1090  
  1091  func (s *storeActionSuite) TestSnapActionOptions(c *C) {
  1092  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1093  		assertRequest(c, r, "POST", snapActionPath)
  1094  		// check device authorization is set, implicitly checking doRequest was used
  1095  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1096  
  1097  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "true")
  1098  
  1099  		jsonReq, err := ioutil.ReadAll(r.Body)
  1100  		c.Assert(err, IsNil)
  1101  		var req struct {
  1102  			Context []map[string]interface{} `json:"context"`
  1103  			Actions []map[string]interface{} `json:"actions"`
  1104  		}
  1105  
  1106  		err = json.Unmarshal(jsonReq, &req)
  1107  		c.Assert(err, IsNil)
  1108  
  1109  		c.Assert(req.Context, HasLen, 1)
  1110  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1111  			"snap-id":          helloWorldSnapID,
  1112  			"instance-key":     helloWorldSnapID,
  1113  			"revision":         float64(1),
  1114  			"tracking-channel": "stable",
  1115  			"refreshed-date":   helloRefreshedDateStr,
  1116  			"epoch":            iZeroEpoch,
  1117  		})
  1118  		c.Assert(req.Actions, HasLen, 1)
  1119  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1120  			"action":       "refresh",
  1121  			"instance-key": helloWorldSnapID,
  1122  			"snap-id":      helloWorldSnapID,
  1123  			"channel":      "stable",
  1124  		})
  1125  
  1126  		io.WriteString(w, `{
  1127    "results": [{
  1128       "result": "refresh",
  1129       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1130       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1131       "name": "hello-world",
  1132       "snap": {
  1133         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1134         "name": "hello-world",
  1135         "revision": 26,
  1136         "version": "6.1",
  1137         "publisher": {
  1138            "id": "canonical",
  1139            "username": "canonical",
  1140            "display-name": "Canonical"
  1141         }
  1142       }
  1143    }]
  1144  }`)
  1145  	}))
  1146  
  1147  	c.Assert(mockServer, NotNil)
  1148  	defer mockServer.Close()
  1149  
  1150  	mockServerURL, _ := url.Parse(mockServer.URL)
  1151  	cfg := store.Config{
  1152  		StoreBaseURL: mockServerURL,
  1153  	}
  1154  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1155  	sto := store.New(&cfg, dauthCtx)
  1156  
  1157  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1158  		{
  1159  			InstanceName:    "hello-world",
  1160  			SnapID:          helloWorldSnapID,
  1161  			TrackingChannel: "stable",
  1162  			Revision:        snap.R(1),
  1163  			RefreshedDate:   helloRefreshedDate,
  1164  		},
  1165  	}, []*store.SnapAction{
  1166  		{
  1167  			Action:       "refresh",
  1168  			SnapID:       helloWorldSnapID,
  1169  			InstanceName: "hello-world",
  1170  			Channel:      "stable",
  1171  		},
  1172  	}, nil, nil, &store.RefreshOptions{RefreshManaged: true})
  1173  	c.Assert(err, IsNil)
  1174  	c.Assert(results, HasLen, 1)
  1175  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1176  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1177  }
  1178  
  1179  func (s *storeActionSuite) TestSnapActionInstall(c *C) {
  1180  	s.testSnapActionGet("install", "", "", c)
  1181  }
  1182  func (s *storeActionSuite) TestSnapActionInstallWithCohort(c *C) {
  1183  	s.testSnapActionGet("install", "what", "", c)
  1184  }
  1185  func (s *storeActionSuite) TestSnapActionDownload(c *C) {
  1186  	s.testSnapActionGet("download", "", "", c)
  1187  }
  1188  func (s *storeActionSuite) TestSnapActionDownloadWithCohort(c *C) {
  1189  	s.testSnapActionGet("download", "here", "", c)
  1190  }
  1191  func (s *storeActionSuite) TestSnapActionInstallRedirect(c *C) {
  1192  	s.testSnapActionGet("install", "", "2.0/candidate", c)
  1193  }
  1194  func (s *storeActionSuite) TestSnapActionDownloadRedirect(c *C) {
  1195  	s.testSnapActionGet("download", "", "2.0/candidate", c)
  1196  }
  1197  func (s *storeActionSuite) testSnapActionGet(action, cohort, redirectChannel string, c *C) {
  1198  	// action here is one of install or download
  1199  	restore := release.MockOnClassic(false)
  1200  	defer restore()
  1201  
  1202  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1203  		assertRequest(c, r, "POST", snapActionPath)
  1204  		// check device authorization is set, implicitly checking doRequest was used
  1205  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1206  
  1207  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
  1208  
  1209  		// no store ID by default
  1210  		storeID := r.Header.Get("Snap-Device-Store")
  1211  		c.Check(storeID, Equals, "")
  1212  
  1213  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
  1214  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
  1215  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
  1216  
  1217  		jsonReq, err := ioutil.ReadAll(r.Body)
  1218  		c.Assert(err, IsNil)
  1219  		var req struct {
  1220  			Context []map[string]interface{} `json:"context"`
  1221  			Actions []map[string]interface{} `json:"actions"`
  1222  		}
  1223  
  1224  		err = json.Unmarshal(jsonReq, &req)
  1225  		c.Assert(err, IsNil)
  1226  
  1227  		c.Assert(req.Context, HasLen, 0)
  1228  		c.Assert(req.Actions, HasLen, 1)
  1229  		expectedAction := map[string]interface{}{
  1230  			"action":       action,
  1231  			"instance-key": action + "-1",
  1232  			"name":         "hello-world",
  1233  			"channel":      "beta",
  1234  			"epoch":        nil,
  1235  		}
  1236  		if cohort != "" {
  1237  			expectedAction["cohort-key"] = cohort
  1238  		}
  1239  		c.Assert(req.Actions[0], DeepEquals, expectedAction)
  1240  
  1241  		fmt.Fprintf(w, `{
  1242    "results": [{
  1243       "result": "%s",
  1244       "instance-key": "%[1]s-1",
  1245       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1246       "name": "hello-world",
  1247       "effective-channel": "candidate",
  1248       "redirect-channel": "%s",
  1249       "snap": {
  1250         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1251         "name": "hello-world",
  1252         "revision": 26,
  1253         "version": "6.1",
  1254         "publisher": {
  1255            "id": "canonical",
  1256            "username": "canonical",
  1257            "display-name": "Canonical"
  1258         }
  1259       }
  1260    }]
  1261  }`, action, redirectChannel)
  1262  	}))
  1263  
  1264  	c.Assert(mockServer, NotNil)
  1265  	defer mockServer.Close()
  1266  
  1267  	mockServerURL, _ := url.Parse(mockServer.URL)
  1268  	cfg := store.Config{
  1269  		StoreBaseURL: mockServerURL,
  1270  	}
  1271  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1272  	sto := store.New(&cfg, dauthCtx)
  1273  
  1274  	results, _, err := sto.SnapAction(s.ctx, nil,
  1275  		[]*store.SnapAction{
  1276  			{
  1277  				Action:       action,
  1278  				InstanceName: "hello-world",
  1279  				Channel:      "beta",
  1280  				CohortKey:    cohort,
  1281  			},
  1282  		}, nil, nil, nil)
  1283  	c.Assert(err, IsNil)
  1284  	c.Assert(results, HasLen, 1)
  1285  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1286  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1287  	c.Assert(results[0].Version, Equals, "6.1")
  1288  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1289  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1290  	c.Assert(results[0].Deltas, HasLen, 0)
  1291  	// effective-channel
  1292  	c.Assert(results[0].Channel, Equals, "candidate")
  1293  	c.Assert(results[0].RedirectChannel, Equals, redirectChannel)
  1294  }
  1295  
  1296  func (s *storeActionSuite) TestSnapActionInstallAmend(c *C) {
  1297  	// this is what amend would look like
  1298  	restore := release.MockOnClassic(false)
  1299  	defer restore()
  1300  
  1301  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1302  		assertRequest(c, r, "POST", snapActionPath)
  1303  		// check device authorization is set, implicitly checking doRequest was used
  1304  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1305  
  1306  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
  1307  
  1308  		// no store ID by default
  1309  		storeID := r.Header.Get("Snap-Device-Store")
  1310  		c.Check(storeID, Equals, "")
  1311  
  1312  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
  1313  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
  1314  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
  1315  
  1316  		jsonReq, err := ioutil.ReadAll(r.Body)
  1317  		c.Assert(err, IsNil)
  1318  		var req struct {
  1319  			Context []map[string]interface{} `json:"context"`
  1320  			Actions []map[string]interface{} `json:"actions"`
  1321  		}
  1322  
  1323  		err = json.Unmarshal(jsonReq, &req)
  1324  		c.Assert(err, IsNil)
  1325  
  1326  		c.Assert(req.Context, HasLen, 0)
  1327  		c.Assert(req.Actions, HasLen, 1)
  1328  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1329  			"action":       "install",
  1330  			"instance-key": "install-1",
  1331  			"name":         "hello-world",
  1332  			"channel":      "beta",
  1333  			"epoch":        map[string]interface{}{"read": []interface{}{0., 1.}, "write": []interface{}{1.}},
  1334  		})
  1335  
  1336  		fmt.Fprint(w, `{
  1337    "results": [{
  1338       "result": "install",
  1339       "instance-key": "install-1",
  1340       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1341       "name": "hello-world",
  1342       "effective-channel": "candidate",
  1343       "snap": {
  1344         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1345         "name": "hello-world",
  1346         "revision": 26,
  1347         "version": "6.1",
  1348         "publisher": {
  1349            "id": "canonical",
  1350            "username": "canonical",
  1351            "display-name": "Canonical"
  1352         }
  1353       }
  1354    }]
  1355  }`)
  1356  	}))
  1357  
  1358  	c.Assert(mockServer, NotNil)
  1359  	defer mockServer.Close()
  1360  
  1361  	mockServerURL, _ := url.Parse(mockServer.URL)
  1362  	cfg := store.Config{
  1363  		StoreBaseURL: mockServerURL,
  1364  	}
  1365  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1366  	sto := store.New(&cfg, dauthCtx)
  1367  
  1368  	results, _, err := sto.SnapAction(s.ctx, nil,
  1369  		[]*store.SnapAction{
  1370  			{
  1371  				Action:       "install",
  1372  				InstanceName: "hello-world",
  1373  				Channel:      "beta",
  1374  				Epoch:        snap.E("1*"),
  1375  			},
  1376  		}, nil, nil, nil)
  1377  	c.Assert(err, IsNil)
  1378  	c.Assert(results, HasLen, 1)
  1379  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1380  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1381  	c.Assert(results[0].Version, Equals, "6.1")
  1382  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1383  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1384  	c.Assert(results[0].Deltas, HasLen, 0)
  1385  	// effective-channel
  1386  	c.Assert(results[0].Channel, Equals, "candidate")
  1387  }
  1388  
  1389  func (s *storeActionSuite) TestSnapActionWithClientUserAgent(c *C) {
  1390  	restore := release.MockOnClassic(false)
  1391  	defer restore()
  1392  
  1393  	serverCalls := 0
  1394  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1395  		serverCalls++
  1396  		assertRequest(c, r, "POST", snapActionPath)
  1397  
  1398  		c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, "some-snap-agent/1.0")
  1399  
  1400  		io.WriteString(w, `{
  1401    "results": []
  1402  }`)
  1403  	}))
  1404  
  1405  	c.Assert(mockServer, NotNil)
  1406  	defer mockServer.Close()
  1407  
  1408  	mockServerURL, _ := url.Parse(mockServer.URL)
  1409  	cfg := store.Config{
  1410  		StoreBaseURL: mockServerURL,
  1411  	}
  1412  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1413  	sto := store.New(&cfg, dauthCtx)
  1414  
  1415  	// to construct the client-user-agent context we need to
  1416  	// create a req that simulates what the req that the daemon got
  1417  	r, err := http.NewRequest("POST", "/snapd/api", nil)
  1418  	r.Header.Set("User-Agent", "some-snap-agent/1.0")
  1419  	c.Assert(err, IsNil)
  1420  	ctx := store.WithClientUserAgent(s.ctx, r)
  1421  
  1422  	results, _, err := sto.SnapAction(ctx, nil, []*store.SnapAction{{Action: "install", InstanceName: "some-snap"}}, nil, nil, nil)
  1423  	c.Check(serverCalls, Equals, 1)
  1424  	c.Check(results, HasLen, 0)
  1425  	c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true})
  1426  }
  1427  
  1428  func (s *storeActionSuite) TestSnapActionDownloadParallelInstanceKey(c *C) {
  1429  	// action here is one of install or download
  1430  	restore := release.MockOnClassic(false)
  1431  	defer restore()
  1432  
  1433  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1434  		c.Fatal("should not be reached")
  1435  	}))
  1436  
  1437  	c.Assert(mockServer, NotNil)
  1438  	defer mockServer.Close()
  1439  
  1440  	mockServerURL, _ := url.Parse(mockServer.URL)
  1441  	cfg := store.Config{
  1442  		StoreBaseURL: mockServerURL,
  1443  	}
  1444  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1445  	sto := store.New(&cfg, dauthCtx)
  1446  
  1447  	_, _, err := sto.SnapAction(s.ctx, nil,
  1448  		[]*store.SnapAction{
  1449  			{
  1450  				Action:       "download",
  1451  				InstanceName: "hello-world_foo",
  1452  				Channel:      "beta",
  1453  			},
  1454  		}, nil, nil, nil)
  1455  	c.Assert(err, ErrorMatches, `internal error: unsupported download with instance name "hello-world_foo"`)
  1456  }
  1457  
  1458  func (s *storeActionSuite) TestSnapActionInstallWithRevision(c *C) {
  1459  	s.testSnapActionGetWithRevision("install", c)
  1460  }
  1461  
  1462  func (s *storeActionSuite) TestSnapActionDownloadWithRevision(c *C) {
  1463  	s.testSnapActionGetWithRevision("download", c)
  1464  }
  1465  
  1466  func (s *storeActionSuite) testSnapActionGetWithRevision(action string, c *C) {
  1467  	// action here is one of install or download
  1468  	restore := release.MockOnClassic(false)
  1469  	defer restore()
  1470  
  1471  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1472  		assertRequest(c, r, "POST", snapActionPath)
  1473  		// check device authorization is set, implicitly checking doRequest was used
  1474  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1475  
  1476  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
  1477  
  1478  		// no store ID by default
  1479  		storeID := r.Header.Get("Snap-Device-Store")
  1480  		c.Check(storeID, Equals, "")
  1481  
  1482  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
  1483  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
  1484  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
  1485  
  1486  		jsonReq, err := ioutil.ReadAll(r.Body)
  1487  		c.Assert(err, IsNil)
  1488  		var req struct {
  1489  			Context []map[string]interface{} `json:"context"`
  1490  			Actions []map[string]interface{} `json:"actions"`
  1491  		}
  1492  
  1493  		err = json.Unmarshal(jsonReq, &req)
  1494  		c.Assert(err, IsNil)
  1495  
  1496  		c.Assert(req.Context, HasLen, 0)
  1497  		c.Assert(req.Actions, HasLen, 1)
  1498  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1499  			"action":       action,
  1500  			"instance-key": action + "-1",
  1501  			"name":         "hello-world",
  1502  			"revision":     float64(28),
  1503  			"epoch":        nil,
  1504  		})
  1505  
  1506  		fmt.Fprintf(w, `{
  1507    "results": [{
  1508       "result": "%s",
  1509       "instance-key": "%[1]s-1",
  1510       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1511       "name": "hello-world",
  1512       "snap": {
  1513         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1514         "name": "hello-world",
  1515         "revision": 28,
  1516         "version": "6.1",
  1517         "publisher": {
  1518            "id": "canonical",
  1519            "username": "canonical",
  1520            "display-name": "Canonical"
  1521         }
  1522       }
  1523    }]
  1524  }`, action)
  1525  	}))
  1526  
  1527  	c.Assert(mockServer, NotNil)
  1528  	defer mockServer.Close()
  1529  
  1530  	mockServerURL, _ := url.Parse(mockServer.URL)
  1531  	cfg := store.Config{
  1532  		StoreBaseURL: mockServerURL,
  1533  	}
  1534  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1535  	sto := store.New(&cfg, dauthCtx)
  1536  
  1537  	results, _, err := sto.SnapAction(s.ctx, nil,
  1538  		[]*store.SnapAction{
  1539  			{
  1540  				Action:       action,
  1541  				InstanceName: "hello-world",
  1542  				Revision:     snap.R(28),
  1543  			},
  1544  		}, nil, nil, nil)
  1545  	c.Assert(err, IsNil)
  1546  	c.Assert(results, HasLen, 1)
  1547  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1548  	c.Assert(results[0].Revision, Equals, snap.R(28))
  1549  	c.Assert(results[0].Version, Equals, "6.1")
  1550  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1551  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1552  	c.Assert(results[0].Deltas, HasLen, 0)
  1553  	// effective-channel is not set
  1554  	c.Assert(results[0].Channel, Equals, "")
  1555  }
  1556  
  1557  func (s *storeActionSuite) TestSnapActionRevisionNotAvailable(c *C) {
  1558  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1559  		assertRequest(c, r, "POST", snapActionPath)
  1560  		// check device authorization is set, implicitly checking doRequest was used
  1561  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1562  
  1563  		jsonReq, err := ioutil.ReadAll(r.Body)
  1564  		c.Assert(err, IsNil)
  1565  		var req struct {
  1566  			Context []map[string]interface{} `json:"context"`
  1567  			Actions []map[string]interface{} `json:"actions"`
  1568  		}
  1569  
  1570  		err = json.Unmarshal(jsonReq, &req)
  1571  		c.Assert(err, IsNil)
  1572  
  1573  		c.Assert(req.Context, HasLen, 2)
  1574  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1575  			"snap-id":          helloWorldSnapID,
  1576  			"instance-key":     helloWorldSnapID,
  1577  			"revision":         float64(26),
  1578  			"tracking-channel": "stable",
  1579  			"refreshed-date":   helloRefreshedDateStr,
  1580  			"epoch":            iZeroEpoch,
  1581  		})
  1582  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  1583  			"snap-id":          "snap2-id",
  1584  			"instance-key":     "snap2-id",
  1585  			"revision":         float64(2),
  1586  			"tracking-channel": "edge",
  1587  			"refreshed-date":   helloRefreshedDateStr,
  1588  			"epoch":            iZeroEpoch,
  1589  		})
  1590  		c.Assert(req.Actions, HasLen, 4)
  1591  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1592  			"action":       "refresh",
  1593  			"instance-key": helloWorldSnapID,
  1594  			"snap-id":      helloWorldSnapID,
  1595  		})
  1596  		c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{
  1597  			"action":       "refresh",
  1598  			"instance-key": "snap2-id",
  1599  			"snap-id":      "snap2-id",
  1600  			"channel":      "candidate",
  1601  		})
  1602  		c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{
  1603  			"action":       "install",
  1604  			"instance-key": "install-1",
  1605  			"name":         "foo",
  1606  			"channel":      "stable",
  1607  			"epoch":        nil,
  1608  		})
  1609  		c.Assert(req.Actions[3], DeepEquals, map[string]interface{}{
  1610  			"action":       "download",
  1611  			"instance-key": "download-1",
  1612  			"name":         "bar",
  1613  			"revision":     42.,
  1614  			"epoch":        nil,
  1615  		})
  1616  
  1617  		io.WriteString(w, `{
  1618    "results": [{
  1619       "result": "error",
  1620       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1621       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1622       "name": "hello-world",
  1623       "error": {
  1624         "code": "revision-not-found",
  1625         "message": "msg1"
  1626       }
  1627    }, {
  1628       "result": "error",
  1629       "instance-key": "snap2-id",
  1630       "snap-id": "snap2-id",
  1631       "name": "snap2",
  1632       "error": {
  1633         "code": "revision-not-found",
  1634         "message": "msg1",
  1635         "extra": {
  1636           "releases": [{"architecture": "amd64", "channel": "beta"},
  1637                        {"architecture": "arm64", "channel": "beta"}]
  1638         }
  1639       }
  1640    }, {
  1641       "result": "error",
  1642       "instance-key": "install-1",
  1643       "snap-id": "foo-id",
  1644       "name": "foo",
  1645       "error": {
  1646         "code": "revision-not-found",
  1647         "message": "msg2"
  1648       }
  1649    }, {
  1650       "result": "error",
  1651       "instance-key": "download-1",
  1652       "snap-id": "bar-id",
  1653       "name": "bar",
  1654       "error": {
  1655         "code": "revision-not-found",
  1656         "message": "msg3"
  1657       }
  1658    }]
  1659  }`)
  1660  	}))
  1661  
  1662  	c.Assert(mockServer, NotNil)
  1663  	defer mockServer.Close()
  1664  
  1665  	mockServerURL, _ := url.Parse(mockServer.URL)
  1666  	cfg := store.Config{
  1667  		StoreBaseURL: mockServerURL,
  1668  	}
  1669  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1670  	sto := store.New(&cfg, dauthCtx)
  1671  
  1672  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1673  		{
  1674  			InstanceName:    "hello-world",
  1675  			SnapID:          helloWorldSnapID,
  1676  			TrackingChannel: "stable",
  1677  			Revision:        snap.R(26),
  1678  			RefreshedDate:   helloRefreshedDate,
  1679  		},
  1680  		{
  1681  			InstanceName:    "snap2",
  1682  			SnapID:          "snap2-id",
  1683  			TrackingChannel: "edge",
  1684  			Revision:        snap.R(2),
  1685  			RefreshedDate:   helloRefreshedDate,
  1686  		},
  1687  	}, []*store.SnapAction{
  1688  		{
  1689  			Action:       "refresh",
  1690  			InstanceName: "hello-world",
  1691  			SnapID:       helloWorldSnapID,
  1692  		}, {
  1693  			Action:       "refresh",
  1694  			InstanceName: "snap2",
  1695  			SnapID:       "snap2-id",
  1696  			Channel:      "candidate",
  1697  		}, {
  1698  			Action:       "install",
  1699  			InstanceName: "foo",
  1700  			Channel:      "stable",
  1701  		}, {
  1702  			Action:       "download",
  1703  			InstanceName: "bar",
  1704  			Revision:     snap.R(42),
  1705  		},
  1706  	}, nil, nil, nil)
  1707  	c.Assert(results, HasLen, 0)
  1708  	c.Check(err, DeepEquals, &store.SnapActionError{
  1709  		Refresh: map[string]error{
  1710  			"hello-world": &store.RevisionNotAvailableError{
  1711  				Action:  "refresh",
  1712  				Channel: "stable",
  1713  			},
  1714  			"snap2": &store.RevisionNotAvailableError{
  1715  				Action:  "refresh",
  1716  				Channel: "candidate",
  1717  				Releases: []channel.Channel{
  1718  					snaptest.MustParseChannel("beta", "amd64"),
  1719  					snaptest.MustParseChannel("beta", "arm64"),
  1720  				},
  1721  			},
  1722  		},
  1723  		Install: map[string]error{
  1724  			"foo": &store.RevisionNotAvailableError{
  1725  				Action:  "install",
  1726  				Channel: "stable",
  1727  			},
  1728  		},
  1729  		Download: map[string]error{
  1730  			"bar": &store.RevisionNotAvailableError{
  1731  				Action:  "download",
  1732  				Channel: "",
  1733  			},
  1734  		},
  1735  	})
  1736  }
  1737  
  1738  func (s *storeActionSuite) TestSnapActionSnapNotFound(c *C) {
  1739  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1740  		assertRequest(c, r, "POST", snapActionPath)
  1741  		// check device authorization is set, implicitly checking doRequest was used
  1742  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1743  
  1744  		jsonReq, err := ioutil.ReadAll(r.Body)
  1745  		c.Assert(err, IsNil)
  1746  		var req struct {
  1747  			Context []map[string]interface{} `json:"context"`
  1748  			Actions []map[string]interface{} `json:"actions"`
  1749  		}
  1750  
  1751  		err = json.Unmarshal(jsonReq, &req)
  1752  		c.Assert(err, IsNil)
  1753  
  1754  		c.Assert(req.Context, HasLen, 1)
  1755  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1756  			"snap-id":          helloWorldSnapID,
  1757  			"instance-key":     helloWorldSnapID,
  1758  			"revision":         float64(26),
  1759  			"tracking-channel": "stable",
  1760  			"refreshed-date":   helloRefreshedDateStr,
  1761  			"epoch":            iZeroEpoch,
  1762  		})
  1763  		c.Assert(req.Actions, HasLen, 3)
  1764  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1765  			"action":       "refresh",
  1766  			"instance-key": helloWorldSnapID,
  1767  			"snap-id":      helloWorldSnapID,
  1768  			"channel":      "stable",
  1769  		})
  1770  		c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{
  1771  			"action":       "install",
  1772  			"instance-key": "install-1",
  1773  			"name":         "foo",
  1774  			"channel":      "stable",
  1775  			"epoch":        nil,
  1776  		})
  1777  		c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{
  1778  			"action":       "download",
  1779  			"instance-key": "download-1",
  1780  			"name":         "bar",
  1781  			"revision":     42.,
  1782  			"epoch":        nil,
  1783  		})
  1784  
  1785  		io.WriteString(w, `{
  1786    "results": [{
  1787       "result": "error",
  1788       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1789       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1790       "error": {
  1791         "code": "id-not-found",
  1792         "message": "msg1"
  1793       }
  1794    }, {
  1795       "result": "error",
  1796       "instance-key": "install-1",
  1797       "name": "foo",
  1798       "error": {
  1799         "code": "name-not-found",
  1800         "message": "msg2"
  1801       }
  1802    }, {
  1803       "result": "error",
  1804       "instance-key": "download-1",
  1805       "name": "bar",
  1806       "error": {
  1807         "code": "name-not-found",
  1808         "message": "msg3"
  1809       }
  1810    }]
  1811  }`)
  1812  	}))
  1813  
  1814  	c.Assert(mockServer, NotNil)
  1815  	defer mockServer.Close()
  1816  
  1817  	mockServerURL, _ := url.Parse(mockServer.URL)
  1818  	cfg := store.Config{
  1819  		StoreBaseURL: mockServerURL,
  1820  	}
  1821  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1822  	sto := store.New(&cfg, dauthCtx)
  1823  
  1824  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1825  		{
  1826  			InstanceName:    "hello-world",
  1827  			SnapID:          helloWorldSnapID,
  1828  			TrackingChannel: "stable",
  1829  			Revision:        snap.R(26),
  1830  			RefreshedDate:   helloRefreshedDate,
  1831  		},
  1832  	}, []*store.SnapAction{
  1833  		{
  1834  			Action:       "refresh",
  1835  			SnapID:       helloWorldSnapID,
  1836  			InstanceName: "hello-world",
  1837  			Channel:      "stable",
  1838  		}, {
  1839  			Action:       "install",
  1840  			InstanceName: "foo",
  1841  			Channel:      "stable",
  1842  		}, {
  1843  			Action:       "download",
  1844  			InstanceName: "bar",
  1845  			Revision:     snap.R(42),
  1846  		},
  1847  	}, nil, nil, nil)
  1848  	c.Assert(results, HasLen, 0)
  1849  	c.Check(err, DeepEquals, &store.SnapActionError{
  1850  		Refresh: map[string]error{
  1851  			"hello-world": store.ErrSnapNotFound,
  1852  		},
  1853  		Install: map[string]error{
  1854  			"foo": store.ErrSnapNotFound,
  1855  		},
  1856  		Download: map[string]error{
  1857  			"bar": store.ErrSnapNotFound,
  1858  		},
  1859  	})
  1860  }
  1861  
  1862  func (s *storeActionSuite) TestSnapActionOtherErrors(c *C) {
  1863  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1864  		assertRequest(c, r, "POST", snapActionPath)
  1865  		// check device authorization is set, implicitly checking doRequest was used
  1866  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1867  
  1868  		jsonReq, err := ioutil.ReadAll(r.Body)
  1869  		c.Assert(err, IsNil)
  1870  		var req struct {
  1871  			Context []map[string]interface{} `json:"context"`
  1872  			Actions []map[string]interface{} `json:"actions"`
  1873  		}
  1874  
  1875  		err = json.Unmarshal(jsonReq, &req)
  1876  		c.Assert(err, IsNil)
  1877  
  1878  		c.Assert(req.Context, HasLen, 0)
  1879  		c.Assert(req.Actions, HasLen, 1)
  1880  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1881  			"action":       "install",
  1882  			"instance-key": "install-1",
  1883  			"name":         "foo",
  1884  			"channel":      "stable",
  1885  			"epoch":        nil,
  1886  		})
  1887  
  1888  		io.WriteString(w, `{
  1889    "results": [{
  1890       "result": "error",
  1891       "error": {
  1892         "code": "other1",
  1893         "message": "other error one"
  1894       }
  1895    }],
  1896    "error-list": [
  1897       {"code": "global-error", "message": "global error"}
  1898    ]
  1899  }`)
  1900  	}))
  1901  
  1902  	c.Assert(mockServer, NotNil)
  1903  	defer mockServer.Close()
  1904  
  1905  	mockServerURL, _ := url.Parse(mockServer.URL)
  1906  	cfg := store.Config{
  1907  		StoreBaseURL: mockServerURL,
  1908  	}
  1909  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1910  	sto := store.New(&cfg, dauthCtx)
  1911  
  1912  	results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{
  1913  		{
  1914  			Action:       "install",
  1915  			InstanceName: "foo",
  1916  			Channel:      "stable",
  1917  		},
  1918  	}, nil, nil, nil)
  1919  	c.Assert(results, HasLen, 0)
  1920  	c.Check(err, DeepEquals, &store.SnapActionError{
  1921  		Other: []error{
  1922  			fmt.Errorf("other error one"),
  1923  			fmt.Errorf("global error"),
  1924  		},
  1925  	})
  1926  }
  1927  
  1928  func (s *storeActionSuite) TestSnapActionUnknownAction(c *C) {
  1929  	restore := release.MockOnClassic(false)
  1930  	defer restore()
  1931  
  1932  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1933  		c.Fatal("should not have made it to the server")
  1934  	}))
  1935  
  1936  	c.Assert(mockServer, NotNil)
  1937  	defer mockServer.Close()
  1938  
  1939  	mockServerURL, _ := url.Parse(mockServer.URL)
  1940  	cfg := store.Config{
  1941  		StoreBaseURL: mockServerURL,
  1942  	}
  1943  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1944  	sto := store.New(&cfg, dauthCtx)
  1945  
  1946  	results, _, err := sto.SnapAction(s.ctx, nil,
  1947  		[]*store.SnapAction{
  1948  			{
  1949  				Action:       "something unexpected",
  1950  				InstanceName: "hello-world",
  1951  			},
  1952  		}, nil, nil, nil)
  1953  	c.Assert(err, ErrorMatches, `.* unsupported action .*`)
  1954  	c.Assert(results, IsNil)
  1955  }
  1956  
  1957  func (s *storeActionSuite) TestSnapActionErrorError(c *C) {
  1958  	e := &store.SnapActionError{Refresh: map[string]error{
  1959  		"foo": fmt.Errorf("sad refresh"),
  1960  	}}
  1961  	c.Check(e.Error(), Equals, `cannot refresh snap "foo": sad refresh`)
  1962  
  1963  	op, name, err := e.SingleOpError()
  1964  	c.Check(op, Equals, "refresh")
  1965  	c.Check(name, Equals, "foo")
  1966  	c.Check(err, ErrorMatches, "sad refresh")
  1967  
  1968  	e = &store.SnapActionError{Refresh: map[string]error{
  1969  		"foo": fmt.Errorf("sad refresh 1"),
  1970  		"bar": fmt.Errorf("sad refresh 2"),
  1971  	}}
  1972  	errMsg := e.Error()
  1973  	c.Check(strings.HasPrefix(errMsg, "cannot refresh:"), Equals, true)
  1974  	c.Check(errMsg, testutil.Contains, "\nsad refresh 1: \"foo\"")
  1975  	c.Check(errMsg, testutil.Contains, "\nsad refresh 2: \"bar\"")
  1976  
  1977  	op, name, err = e.SingleOpError()
  1978  	c.Check(op, Equals, "")
  1979  	c.Check(name, Equals, "")
  1980  	c.Check(err, IsNil)
  1981  
  1982  	e = &store.SnapActionError{Install: map[string]error{
  1983  		"foo": fmt.Errorf("sad install"),
  1984  	}}
  1985  	c.Check(e.Error(), Equals, `cannot install snap "foo": sad install`)
  1986  
  1987  	op, name, err = e.SingleOpError()
  1988  	c.Check(op, Equals, "install")
  1989  	c.Check(name, Equals, "foo")
  1990  	c.Check(err, ErrorMatches, "sad install")
  1991  
  1992  	e = &store.SnapActionError{Install: map[string]error{
  1993  		"foo": fmt.Errorf("sad install 1"),
  1994  		"bar": fmt.Errorf("sad install 2"),
  1995  	}}
  1996  	errMsg = e.Error()
  1997  	c.Check(strings.HasPrefix(errMsg, "cannot install:\n"), Equals, true)
  1998  	c.Check(errMsg, testutil.Contains, "\nsad install 1: \"foo\"")
  1999  	c.Check(errMsg, testutil.Contains, "\nsad install 2: \"bar\"")
  2000  
  2001  	op, name, err = e.SingleOpError()
  2002  	c.Check(op, Equals, "")
  2003  	c.Check(name, Equals, "")
  2004  	c.Check(err, IsNil)
  2005  
  2006  	e = &store.SnapActionError{Download: map[string]error{
  2007  		"foo": fmt.Errorf("sad download"),
  2008  	}}
  2009  	c.Check(e.Error(), Equals, `cannot download snap "foo": sad download`)
  2010  
  2011  	op, name, err = e.SingleOpError()
  2012  	c.Check(op, Equals, "download")
  2013  	c.Check(name, Equals, "foo")
  2014  	c.Check(err, ErrorMatches, "sad download")
  2015  
  2016  	e = &store.SnapActionError{Download: map[string]error{
  2017  		"foo": fmt.Errorf("sad download 1"),
  2018  		"bar": fmt.Errorf("sad download 2"),
  2019  	}}
  2020  	errMsg = e.Error()
  2021  	c.Check(strings.HasPrefix(errMsg, "cannot download:\n"), Equals, true)
  2022  	c.Check(errMsg, testutil.Contains, "\nsad download 1: \"foo\"")
  2023  	c.Check(errMsg, testutil.Contains, "\nsad download 2: \"bar\"")
  2024  
  2025  	op, name, err = e.SingleOpError()
  2026  	c.Check(op, Equals, "")
  2027  	c.Check(name, Equals, "")
  2028  	c.Check(err, IsNil)
  2029  
  2030  	e = &store.SnapActionError{Refresh: map[string]error{
  2031  		"foo": fmt.Errorf("sad refresh 1"),
  2032  	},
  2033  		Install: map[string]error{
  2034  			"bar": fmt.Errorf("sad install 2"),
  2035  		}}
  2036  	c.Check(e.Error(), Equals, `cannot refresh or install:
  2037  sad refresh 1: "foo"
  2038  sad install 2: "bar"`)
  2039  
  2040  	op, name, err = e.SingleOpError()
  2041  	c.Check(op, Equals, "")
  2042  	c.Check(name, Equals, "")
  2043  	c.Check(err, IsNil)
  2044  
  2045  	e = &store.SnapActionError{Refresh: map[string]error{
  2046  		"foo": fmt.Errorf("sad refresh 1"),
  2047  	},
  2048  		Download: map[string]error{
  2049  			"bar": fmt.Errorf("sad download 2"),
  2050  		}}
  2051  	c.Check(e.Error(), Equals, `cannot refresh or download:
  2052  sad refresh 1: "foo"
  2053  sad download 2: "bar"`)
  2054  
  2055  	op, name, err = e.SingleOpError()
  2056  	c.Check(op, Equals, "")
  2057  	c.Check(name, Equals, "")
  2058  	c.Check(err, IsNil)
  2059  
  2060  	e = &store.SnapActionError{Install: map[string]error{
  2061  		"foo": fmt.Errorf("sad install 1"),
  2062  	},
  2063  		Download: map[string]error{
  2064  			"bar": fmt.Errorf("sad download 2"),
  2065  		}}
  2066  	c.Check(e.Error(), Equals, `cannot install or download:
  2067  sad install 1: "foo"
  2068  sad download 2: "bar"`)
  2069  
  2070  	op, name, err = e.SingleOpError()
  2071  	c.Check(op, Equals, "")
  2072  	c.Check(name, Equals, "")
  2073  	c.Check(err, IsNil)
  2074  
  2075  	e = &store.SnapActionError{Refresh: map[string]error{
  2076  		"foo": fmt.Errorf("sad refresh 1"),
  2077  	},
  2078  		Install: map[string]error{
  2079  			"bar": fmt.Errorf("sad install 2"),
  2080  		},
  2081  		Download: map[string]error{
  2082  			"baz": fmt.Errorf("sad download 3"),
  2083  		}}
  2084  	c.Check(e.Error(), Equals, `cannot refresh, install, or download:
  2085  sad refresh 1: "foo"
  2086  sad install 2: "bar"
  2087  sad download 3: "baz"`)
  2088  
  2089  	op, name, err = e.SingleOpError()
  2090  	c.Check(op, Equals, "")
  2091  	c.Check(name, Equals, "")
  2092  	c.Check(err, IsNil)
  2093  
  2094  	e = &store.SnapActionError{
  2095  		NoResults: true,
  2096  		Other:     []error{fmt.Errorf("other error")},
  2097  	}
  2098  	c.Check(e.Error(), Equals, `cannot refresh, install, or download: other error`)
  2099  
  2100  	op, name, err = e.SingleOpError()
  2101  	c.Check(op, Equals, "")
  2102  	c.Check(name, Equals, "")
  2103  	c.Check(err, IsNil)
  2104  
  2105  	e = &store.SnapActionError{
  2106  		Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")},
  2107  	}
  2108  	c.Check(e.Error(), Equals, `cannot refresh, install, or download:
  2109  other error 1
  2110  other error 2`)
  2111  
  2112  	op, name, err = e.SingleOpError()
  2113  	c.Check(op, Equals, "")
  2114  	c.Check(name, Equals, "")
  2115  	c.Check(err, IsNil)
  2116  
  2117  	e = &store.SnapActionError{
  2118  		Install: map[string]error{
  2119  			"bar": fmt.Errorf("sad install"),
  2120  		},
  2121  		Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")},
  2122  	}
  2123  	c.Check(e.Error(), Equals, `cannot refresh, install, or download:
  2124  sad install: "bar"
  2125  other error 1
  2126  other error 2`)
  2127  
  2128  	op, name, err = e.SingleOpError()
  2129  	c.Check(op, Equals, "")
  2130  	c.Check(name, Equals, "")
  2131  	c.Check(err, IsNil)
  2132  
  2133  	e = &store.SnapActionError{
  2134  		NoResults: true,
  2135  	}
  2136  	c.Check(e.Error(), Equals, "no install/refresh information results from the store")
  2137  
  2138  	op, name, err = e.SingleOpError()
  2139  	c.Check(op, Equals, "")
  2140  	c.Check(name, Equals, "")
  2141  	c.Check(err, IsNil)
  2142  }
  2143  
  2144  func (s *storeActionSuite) TestSnapActionRefreshesBothAuths(c *C) {
  2145  	// snap action (install/refresh) has is its own custom way to
  2146  	// signal macaroon refreshes that allows to do a best effort
  2147  	// with the available results
  2148  
  2149  	refresh, err := makeTestRefreshDischargeResponse()
  2150  	c.Assert(err, IsNil)
  2151  	c.Check(s.user.StoreDischarges[0], Not(Equals), refresh)
  2152  
  2153  	// mock refresh response
  2154  	refreshDischargeEndpointHit := false
  2155  	mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2156  		io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
  2157  		refreshDischargeEndpointHit = true
  2158  	}))
  2159  	defer mockSSOServer.Close()
  2160  	store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
  2161  
  2162  	refreshSessionRequested := false
  2163  	expiredAuth := `Macaroon root="expired-session-macaroon"`
  2164  	n := 0
  2165  	// mock store response
  2166  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2167  		c.Check(r.UserAgent(), Equals, userAgent)
  2168  
  2169  		switch r.URL.Path {
  2170  		case snapActionPath:
  2171  			n++
  2172  			type errObj struct {
  2173  				Code    string `json:"code"`
  2174  				Message string `json:"message"`
  2175  			}
  2176  			var errors []errObj
  2177  
  2178  			authorization := r.Header.Get("Authorization")
  2179  			c.Check(authorization, Equals, expectedAuthorization(c, s.user))
  2180  			if s.user.StoreDischarges[0] != refresh {
  2181  				errors = append(errors, errObj{Code: "user-authorization-needs-refresh"})
  2182  			}
  2183  
  2184  			devAuthorization := r.Header.Get("Snap-Device-Authorization")
  2185  			if devAuthorization == "" {
  2186  				c.Fatalf("device authentication missing")
  2187  			} else if devAuthorization == expiredAuth {
  2188  				errors = append(errors, errObj{Code: "device-authorization-needs-refresh"})
  2189  			} else {
  2190  				c.Check(devAuthorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
  2191  			}
  2192  
  2193  			errorsJSON, err := json.Marshal(errors)
  2194  			c.Assert(err, IsNil)
  2195  
  2196  			io.WriteString(w, fmt.Sprintf(`{
  2197    "results": [{
  2198       "result": "refresh",
  2199       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2200       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2201       "name": "hello-world",
  2202       "snap": {
  2203         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2204         "name": "hello-world",
  2205         "revision": 26,
  2206         "version": "6.1",
  2207         "publisher": {
  2208            "id": "canonical",
  2209            "name": "canonical",
  2210            "title": "Canonical"
  2211         }
  2212       }
  2213    }],
  2214    "error-list": %s
  2215  }`, errorsJSON))
  2216  		case authNoncesPath:
  2217  			io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
  2218  		case authSessionPath:
  2219  			// sanity of request
  2220  			jsonReq, err := ioutil.ReadAll(r.Body)
  2221  			c.Assert(err, IsNil)
  2222  			var req map[string]string
  2223  			err = json.Unmarshal(jsonReq, &req)
  2224  			c.Assert(err, IsNil)
  2225  			c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true)
  2226  			c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true)
  2227  			c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true)
  2228  
  2229  			authorization := r.Header.Get("X-Device-Authorization")
  2230  			if authorization == "" {
  2231  				c.Fatalf("expecting only refresh")
  2232  			} else {
  2233  				c.Check(authorization, Equals, expiredAuth)
  2234  				io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
  2235  				refreshSessionRequested = true
  2236  			}
  2237  		default:
  2238  			c.Fatalf("unexpected path %q", r.URL.Path)
  2239  		}
  2240  	}))
  2241  	c.Assert(mockServer, NotNil)
  2242  	defer mockServer.Close()
  2243  
  2244  	mockServerURL, _ := url.Parse(mockServer.URL)
  2245  
  2246  	// make sure device session is expired
  2247  	s.device.SessionMacaroon = "expired-session-macaroon"
  2248  	dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user}
  2249  	sto := store.New(&store.Config{
  2250  		StoreBaseURL: mockServerURL,
  2251  	}, dauthCtx)
  2252  
  2253  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2254  		{
  2255  			InstanceName:    "hello-world",
  2256  			SnapID:          helloWorldSnapID,
  2257  			TrackingChannel: "beta",
  2258  			Revision:        snap.R(1),
  2259  			RefreshedDate:   helloRefreshedDate,
  2260  		},
  2261  	}, []*store.SnapAction{
  2262  		{
  2263  			Action:       "refresh",
  2264  			SnapID:       helloWorldSnapID,
  2265  			InstanceName: "hello-world",
  2266  		},
  2267  	}, nil, s.user, nil)
  2268  	c.Assert(err, IsNil)
  2269  	c.Assert(results, HasLen, 1)
  2270  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  2271  	c.Check(refreshDischargeEndpointHit, Equals, true)
  2272  	c.Check(refreshSessionRequested, Equals, true)
  2273  	c.Check(n, Equals, 2)
  2274  }
  2275  
  2276  func (s *storeActionSuite) TestSnapActionRefreshParallelInstall(c *C) {
  2277  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2278  		assertRequest(c, r, "POST", snapActionPath)
  2279  		// check device authorization is set, implicitly checking doRequest was used
  2280  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2281  
  2282  		jsonReq, err := ioutil.ReadAll(r.Body)
  2283  		c.Assert(err, IsNil)
  2284  		var req struct {
  2285  			Context []map[string]interface{} `json:"context"`
  2286  			Actions []map[string]interface{} `json:"actions"`
  2287  		}
  2288  
  2289  		err = json.Unmarshal(jsonReq, &req)
  2290  		c.Assert(err, IsNil)
  2291  
  2292  		c.Assert(req.Context, HasLen, 2)
  2293  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2294  			"snap-id":          helloWorldSnapID,
  2295  			"instance-key":     helloWorldSnapID,
  2296  			"revision":         float64(26),
  2297  			"tracking-channel": "stable",
  2298  			"refreshed-date":   helloRefreshedDateStr,
  2299  			"epoch":            iZeroEpoch,
  2300  		})
  2301  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2302  			"snap-id":          helloWorldSnapID,
  2303  			"instance-key":     helloWorldFooInstanceKeyWithSalt,
  2304  			"revision":         float64(2),
  2305  			"tracking-channel": "stable",
  2306  			"refreshed-date":   helloRefreshedDateStr,
  2307  			"epoch":            iZeroEpoch,
  2308  		})
  2309  		c.Assert(req.Actions, HasLen, 1)
  2310  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2311  			"action":       "refresh",
  2312  			"instance-key": helloWorldFooInstanceKeyWithSalt,
  2313  			"snap-id":      helloWorldSnapID,
  2314  			"channel":      "stable",
  2315  		})
  2316  
  2317  		io.WriteString(w, `{
  2318    "results": [{
  2319       "result": "refresh",
  2320       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk",
  2321       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2322       "name": "hello-world",
  2323       "snap": {
  2324         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2325         "name": "hello-world",
  2326         "revision": 26,
  2327         "version": "6.1",
  2328         "publisher": {
  2329            "id": "canonical",
  2330            "username": "canonical",
  2331            "display-name": "Canonical"
  2332         }
  2333       }
  2334    }]
  2335  }`)
  2336  	}))
  2337  
  2338  	c.Assert(mockServer, NotNil)
  2339  	defer mockServer.Close()
  2340  
  2341  	mockServerURL, _ := url.Parse(mockServer.URL)
  2342  	cfg := store.Config{
  2343  		StoreBaseURL: mockServerURL,
  2344  	}
  2345  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2346  	sto := store.New(&cfg, dauthCtx)
  2347  
  2348  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2349  		{
  2350  			InstanceName:    "hello-world",
  2351  			SnapID:          helloWorldSnapID,
  2352  			TrackingChannel: "stable",
  2353  			Revision:        snap.R(26),
  2354  			RefreshedDate:   helloRefreshedDate,
  2355  		}, {
  2356  			InstanceName:    "hello-world_foo",
  2357  			SnapID:          helloWorldSnapID,
  2358  			TrackingChannel: "stable",
  2359  			Revision:        snap.R(2),
  2360  			RefreshedDate:   helloRefreshedDate,
  2361  		},
  2362  	}, []*store.SnapAction{
  2363  		{
  2364  			Action:       "refresh",
  2365  			SnapID:       helloWorldSnapID,
  2366  			Channel:      "stable",
  2367  			InstanceName: "hello-world_foo",
  2368  		},
  2369  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  2370  	c.Assert(err, IsNil)
  2371  	c.Assert(results, HasLen, 1)
  2372  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2373  	c.Assert(results[0].InstanceName(), Equals, "hello-world_foo")
  2374  	c.Assert(results[0].Revision, Equals, snap.R(26))
  2375  }
  2376  
  2377  func (s *storeActionSuite) TestSnapActionRefreshStableInstanceKey(c *C) {
  2378  	// salt "foo"
  2379  	helloWorldFooInstanceKeyWithSaltFoo := helloWorldSnapID + ":CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI"
  2380  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2381  		assertRequest(c, r, "POST", snapActionPath)
  2382  		// check device authorization is set, implicitly checking doRequest was used
  2383  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2384  
  2385  		jsonReq, err := ioutil.ReadAll(r.Body)
  2386  		c.Assert(err, IsNil)
  2387  		var req struct {
  2388  			Context []map[string]interface{} `json:"context"`
  2389  			Actions []map[string]interface{} `json:"actions"`
  2390  		}
  2391  
  2392  		err = json.Unmarshal(jsonReq, &req)
  2393  		c.Assert(err, IsNil)
  2394  
  2395  		c.Assert(req.Context, HasLen, 2)
  2396  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2397  			"snap-id":          helloWorldSnapID,
  2398  			"instance-key":     helloWorldSnapID,
  2399  			"revision":         float64(26),
  2400  			"tracking-channel": "stable",
  2401  			"refreshed-date":   helloRefreshedDateStr,
  2402  			"epoch":            iZeroEpoch,
  2403  			"cohort-key":       "what",
  2404  		})
  2405  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2406  			"snap-id":          helloWorldSnapID,
  2407  			"instance-key":     helloWorldFooInstanceKeyWithSaltFoo,
  2408  			"revision":         float64(2),
  2409  			"tracking-channel": "stable",
  2410  			"refreshed-date":   helloRefreshedDateStr,
  2411  			"epoch":            iZeroEpoch,
  2412  		})
  2413  		c.Assert(req.Actions, HasLen, 1)
  2414  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2415  			"action":       "refresh",
  2416  			"instance-key": helloWorldFooInstanceKeyWithSaltFoo,
  2417  			"snap-id":      helloWorldSnapID,
  2418  			"channel":      "stable",
  2419  		})
  2420  
  2421  		io.WriteString(w, `{
  2422    "results": [{
  2423       "result": "refresh",
  2424       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI",
  2425       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2426       "name": "hello-world",
  2427       "snap": {
  2428         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2429         "name": "hello-world",
  2430         "revision": 26,
  2431         "version": "6.1",
  2432         "publisher": {
  2433            "id": "canonical",
  2434            "username": "canonical",
  2435            "display-name": "Canonical"
  2436         }
  2437       }
  2438    }]
  2439  }`)
  2440  	}))
  2441  
  2442  	c.Assert(mockServer, NotNil)
  2443  	defer mockServer.Close()
  2444  
  2445  	mockServerURL, _ := url.Parse(mockServer.URL)
  2446  	cfg := store.Config{
  2447  		StoreBaseURL: mockServerURL,
  2448  	}
  2449  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2450  	sto := store.New(&cfg, dauthCtx)
  2451  
  2452  	opts := &store.RefreshOptions{PrivacyKey: "foo"}
  2453  	currentSnaps := []*store.CurrentSnap{
  2454  		{
  2455  			InstanceName:    "hello-world",
  2456  			SnapID:          helloWorldSnapID,
  2457  			TrackingChannel: "stable",
  2458  			Revision:        snap.R(26),
  2459  			RefreshedDate:   helloRefreshedDate,
  2460  			CohortKey:       "what",
  2461  		}, {
  2462  			InstanceName:    "hello-world_foo",
  2463  			SnapID:          helloWorldSnapID,
  2464  			TrackingChannel: "stable",
  2465  			Revision:        snap.R(2),
  2466  			RefreshedDate:   helloRefreshedDate,
  2467  		},
  2468  	}
  2469  	action := []*store.SnapAction{
  2470  		{
  2471  			Action:       "refresh",
  2472  			SnapID:       helloWorldSnapID,
  2473  			Channel:      "stable",
  2474  			InstanceName: "hello-world_foo",
  2475  		},
  2476  	}
  2477  	results, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts)
  2478  	c.Assert(err, IsNil)
  2479  	c.Assert(results, HasLen, 1)
  2480  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2481  	c.Assert(results[0].InstanceName(), Equals, "hello-world_foo")
  2482  	c.Assert(results[0].Revision, Equals, snap.R(26))
  2483  
  2484  	// another request with the same seed, gives same result
  2485  	resultsAgain, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts)
  2486  	c.Assert(err, IsNil)
  2487  	c.Assert(resultsAgain, DeepEquals, results)
  2488  }
  2489  
  2490  func (s *storeActionSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) {
  2491  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2492  		assertRequest(c, r, "POST", snapActionPath)
  2493  		// check device authorization is set, implicitly checking doRequest was used
  2494  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2495  
  2496  		jsonReq, err := ioutil.ReadAll(r.Body)
  2497  		c.Assert(err, IsNil)
  2498  		var req struct {
  2499  			Context []map[string]interface{} `json:"context"`
  2500  			Actions []map[string]interface{} `json:"actions"`
  2501  		}
  2502  
  2503  		err = json.Unmarshal(jsonReq, &req)
  2504  		c.Assert(err, IsNil)
  2505  
  2506  		c.Assert(req.Context, HasLen, 2)
  2507  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2508  			"snap-id":          helloWorldSnapID,
  2509  			"instance-key":     helloWorldSnapID,
  2510  			"revision":         float64(26),
  2511  			"tracking-channel": "stable",
  2512  			"refreshed-date":   helloRefreshedDateStr,
  2513  			"epoch":            iZeroEpoch,
  2514  		})
  2515  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2516  			"snap-id":          helloWorldSnapID,
  2517  			"instance-key":     helloWorldFooInstanceKeyWithSalt,
  2518  			"revision":         float64(2),
  2519  			"tracking-channel": "edge",
  2520  			"refreshed-date":   helloRefreshedDateStr,
  2521  			"epoch":            iZeroEpoch,
  2522  		})
  2523  		c.Assert(req.Actions, HasLen, 3)
  2524  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2525  			"action":       "refresh",
  2526  			"instance-key": helloWorldSnapID,
  2527  			"snap-id":      helloWorldSnapID,
  2528  		})
  2529  		c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{
  2530  			"action":       "refresh",
  2531  			"instance-key": helloWorldFooInstanceKeyWithSalt,
  2532  			"snap-id":      helloWorldSnapID,
  2533  		})
  2534  		c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{
  2535  			"action":       "install",
  2536  			"instance-key": "install-1",
  2537  			"name":         "other",
  2538  			"channel":      "stable",
  2539  			"epoch":        nil,
  2540  		})
  2541  
  2542  		io.WriteString(w, `{
  2543    "results": [{
  2544       "result": "error",
  2545       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2546       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2547       "name": "hello-world",
  2548       "error": {
  2549         "code": "revision-not-found",
  2550         "message": "msg1"
  2551       }
  2552    }, {
  2553       "result": "error",
  2554       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk",
  2555       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2556       "name": "hello-world",
  2557       "error": {
  2558         "code": "revision-not-found",
  2559         "message": "msg2"
  2560       }
  2561    },  {
  2562       "result": "error",
  2563       "instance-key": "install-1",
  2564       "snap-id": "foo-id",
  2565       "name": "other",
  2566       "error": {
  2567         "code": "revision-not-found",
  2568         "message": "msg3"
  2569       }
  2570    }
  2571    ]
  2572  }`)
  2573  	}))
  2574  
  2575  	c.Assert(mockServer, NotNil)
  2576  	defer mockServer.Close()
  2577  
  2578  	mockServerURL, _ := url.Parse(mockServer.URL)
  2579  	cfg := store.Config{
  2580  		StoreBaseURL: mockServerURL,
  2581  	}
  2582  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2583  	sto := store.New(&cfg, dauthCtx)
  2584  
  2585  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2586  		{
  2587  			InstanceName:    "hello-world",
  2588  			SnapID:          helloWorldSnapID,
  2589  			TrackingChannel: "stable",
  2590  			Revision:        snap.R(26),
  2591  			RefreshedDate:   helloRefreshedDate,
  2592  		},
  2593  		{
  2594  			InstanceName:    "hello-world_foo",
  2595  			SnapID:          helloWorldSnapID,
  2596  			TrackingChannel: "edge",
  2597  			Revision:        snap.R(2),
  2598  			RefreshedDate:   helloRefreshedDate,
  2599  		},
  2600  	}, []*store.SnapAction{
  2601  		{
  2602  			Action:       "refresh",
  2603  			InstanceName: "hello-world",
  2604  			SnapID:       helloWorldSnapID,
  2605  		}, {
  2606  			Action:       "refresh",
  2607  			InstanceName: "hello-world_foo",
  2608  			SnapID:       helloWorldSnapID,
  2609  		}, {
  2610  			Action:       "install",
  2611  			InstanceName: "other_foo",
  2612  			Channel:      "stable",
  2613  		},
  2614  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  2615  	c.Assert(results, HasLen, 0)
  2616  	c.Check(err, DeepEquals, &store.SnapActionError{
  2617  		Refresh: map[string]error{
  2618  			"hello-world": &store.RevisionNotAvailableError{
  2619  				Action:  "refresh",
  2620  				Channel: "stable",
  2621  			},
  2622  			"hello-world_foo": &store.RevisionNotAvailableError{
  2623  				Action:  "refresh",
  2624  				Channel: "edge",
  2625  			},
  2626  		},
  2627  		Install: map[string]error{
  2628  			"other_foo": &store.RevisionNotAvailableError{
  2629  				Action:  "install",
  2630  				Channel: "stable",
  2631  			},
  2632  		},
  2633  	})
  2634  }
  2635  
  2636  func (s *storeActionSuite) TestSnapActionInstallParallelInstall(c *C) {
  2637  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2638  		assertRequest(c, r, "POST", snapActionPath)
  2639  		// check device authorization is set, implicitly checking doRequest was used
  2640  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2641  
  2642  		jsonReq, err := ioutil.ReadAll(r.Body)
  2643  		c.Assert(err, IsNil)
  2644  		var req struct {
  2645  			Context []map[string]interface{} `json:"context"`
  2646  			Actions []map[string]interface{} `json:"actions"`
  2647  		}
  2648  
  2649  		err = json.Unmarshal(jsonReq, &req)
  2650  		c.Assert(err, IsNil)
  2651  
  2652  		c.Assert(req.Context, HasLen, 1)
  2653  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2654  			"snap-id":          helloWorldSnapID,
  2655  			"instance-key":     helloWorldSnapID,
  2656  			"revision":         float64(26),
  2657  			"tracking-channel": "stable",
  2658  			"refreshed-date":   helloRefreshedDateStr,
  2659  			"epoch":            iZeroEpoch,
  2660  		})
  2661  		c.Assert(req.Actions, HasLen, 1)
  2662  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2663  			"action":       "install",
  2664  			"instance-key": "install-1",
  2665  			"name":         "hello-world",
  2666  			"channel":      "stable",
  2667  			"epoch":        nil,
  2668  		})
  2669  
  2670  		io.WriteString(w, `{
  2671    "results": [{
  2672       "result": "install",
  2673       "instance-key": "install-1",
  2674       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2675       "name": "hello-world",
  2676       "snap": {
  2677         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2678         "name": "hello-world",
  2679         "revision": 28,
  2680         "version": "6.1",
  2681         "publisher": {
  2682            "id": "canonical",
  2683            "username": "canonical",
  2684            "display-name": "Canonical"
  2685         }
  2686       }
  2687    }]
  2688  }`)
  2689  	}))
  2690  
  2691  	c.Assert(mockServer, NotNil)
  2692  	defer mockServer.Close()
  2693  
  2694  	mockServerURL, _ := url.Parse(mockServer.URL)
  2695  	cfg := store.Config{
  2696  		StoreBaseURL: mockServerURL,
  2697  	}
  2698  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2699  	sto := store.New(&cfg, dauthCtx)
  2700  
  2701  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2702  		{
  2703  			InstanceName:    "hello-world",
  2704  			SnapID:          helloWorldSnapID,
  2705  			TrackingChannel: "stable",
  2706  			Revision:        snap.R(26),
  2707  			RefreshedDate:   helloRefreshedDate,
  2708  		},
  2709  	}, []*store.SnapAction{
  2710  		{
  2711  			Action:       "install",
  2712  			InstanceName: "hello-world_foo",
  2713  			Channel:      "stable",
  2714  		},
  2715  	}, nil, nil, nil)
  2716  	c.Assert(err, IsNil)
  2717  	c.Assert(results, HasLen, 1)
  2718  	c.Assert(results[0].InstanceName(), Equals, "hello-world_foo")
  2719  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2720  	c.Assert(results[0].Revision, Equals, snap.R(28))
  2721  	c.Assert(results[0].Version, Equals, "6.1")
  2722  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  2723  	c.Assert(results[0].Deltas, HasLen, 0)
  2724  	// effective-channel is not set
  2725  	c.Assert(results[0].Channel, Equals, "")
  2726  }
  2727  
  2728  func (s *storeActionSuite) TestSnapActionErrorsWhenNoInstanceName(c *C) {
  2729  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2730  	sto := store.New(&store.Config{}, dauthCtx)
  2731  
  2732  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2733  		{
  2734  			InstanceName:    "hello-world",
  2735  			SnapID:          helloWorldSnapID,
  2736  			TrackingChannel: "stable",
  2737  			Revision:        snap.R(26),
  2738  			RefreshedDate:   helloRefreshedDate,
  2739  		},
  2740  	}, []*store.SnapAction{
  2741  		{
  2742  			Action:  "install",
  2743  			Channel: "stable",
  2744  		},
  2745  	}, nil, nil, nil)
  2746  	c.Assert(err, ErrorMatches, "internal error: action without instance name")
  2747  	c.Assert(results, IsNil)
  2748  }
  2749  
  2750  func (s *storeActionSuite) TestSnapActionInstallUnexpectedInstallKey(c *C) {
  2751  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2752  		assertRequest(c, r, "POST", snapActionPath)
  2753  		// check device authorization is set, implicitly checking doRequest was used
  2754  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2755  
  2756  		jsonReq, err := ioutil.ReadAll(r.Body)
  2757  		c.Assert(err, IsNil)
  2758  		var req struct {
  2759  			Context []map[string]interface{} `json:"context"`
  2760  			Actions []map[string]interface{} `json:"actions"`
  2761  		}
  2762  
  2763  		err = json.Unmarshal(jsonReq, &req)
  2764  		c.Assert(err, IsNil)
  2765  
  2766  		c.Assert(req.Context, HasLen, 1)
  2767  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2768  			"snap-id":          helloWorldSnapID,
  2769  			"instance-key":     helloWorldSnapID,
  2770  			"revision":         float64(26),
  2771  			"tracking-channel": "stable",
  2772  			"refreshed-date":   helloRefreshedDateStr,
  2773  			"epoch":            iZeroEpoch,
  2774  		})
  2775  		c.Assert(req.Actions, HasLen, 1)
  2776  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2777  			"action":       "install",
  2778  			"instance-key": "install-1",
  2779  			"name":         "hello-world",
  2780  			"channel":      "stable",
  2781  			"epoch":        nil,
  2782  		})
  2783  
  2784  		io.WriteString(w, `{
  2785    "results": [{
  2786       "result": "install",
  2787       "instance-key": "foo-2",
  2788       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2789       "name": "hello-world",
  2790       "snap": {
  2791         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2792         "name": "hello-world",
  2793         "revision": 28,
  2794         "version": "6.1",
  2795         "publisher": {
  2796            "id": "canonical",
  2797            "username": "canonical",
  2798            "display-name": "Canonical"
  2799         }
  2800       }
  2801    }]
  2802  }`)
  2803  	}))
  2804  
  2805  	c.Assert(mockServer, NotNil)
  2806  	defer mockServer.Close()
  2807  
  2808  	mockServerURL, _ := url.Parse(mockServer.URL)
  2809  	cfg := store.Config{
  2810  		StoreBaseURL: mockServerURL,
  2811  	}
  2812  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2813  	sto := store.New(&cfg, dauthCtx)
  2814  
  2815  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2816  		{
  2817  			InstanceName:    "hello-world",
  2818  			SnapID:          helloWorldSnapID,
  2819  			TrackingChannel: "stable",
  2820  			Revision:        snap.R(26),
  2821  			RefreshedDate:   helloRefreshedDate,
  2822  		},
  2823  	}, []*store.SnapAction{
  2824  		{
  2825  			Action:       "install",
  2826  			InstanceName: "hello-world_foo",
  2827  			Channel:      "stable",
  2828  		},
  2829  	}, nil, nil, nil)
  2830  	c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected instance-key "foo-2"`)
  2831  	c.Assert(results, IsNil)
  2832  }
  2833  
  2834  func (s *storeActionSuite) TestSnapActionRefreshUnexpectedInstanceKey(c *C) {
  2835  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2836  		assertRequest(c, r, "POST", snapActionPath)
  2837  		// check device authorization is set, implicitly checking doRequest was used
  2838  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2839  
  2840  		jsonReq, err := ioutil.ReadAll(r.Body)
  2841  		c.Assert(err, IsNil)
  2842  		var req struct {
  2843  			Context []map[string]interface{} `json:"context"`
  2844  			Actions []map[string]interface{} `json:"actions"`
  2845  		}
  2846  
  2847  		err = json.Unmarshal(jsonReq, &req)
  2848  		c.Assert(err, IsNil)
  2849  
  2850  		c.Assert(req.Context, HasLen, 1)
  2851  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2852  			"snap-id":          helloWorldSnapID,
  2853  			"instance-key":     helloWorldSnapID,
  2854  			"revision":         float64(26),
  2855  			"tracking-channel": "stable",
  2856  			"refreshed-date":   helloRefreshedDateStr,
  2857  			"epoch":            iZeroEpoch,
  2858  		})
  2859  		c.Assert(req.Actions, HasLen, 1)
  2860  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2861  			"action":       "refresh",
  2862  			"instance-key": helloWorldSnapID,
  2863  			"snap-id":      helloWorldSnapID,
  2864  			"channel":      "stable",
  2865  		})
  2866  
  2867  		io.WriteString(w, `{
  2868    "results": [{
  2869       "result": "refresh",
  2870       "instance-key": "foo-5",
  2871       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2872       "name": "hello-world",
  2873       "snap": {
  2874         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2875         "name": "hello-world",
  2876         "revision": 26,
  2877         "version": "6.1",
  2878         "publisher": {
  2879            "id": "canonical",
  2880            "username": "canonical",
  2881            "display-name": "Canonical"
  2882         }
  2883       }
  2884    }]
  2885  }`)
  2886  	}))
  2887  
  2888  	c.Assert(mockServer, NotNil)
  2889  	defer mockServer.Close()
  2890  
  2891  	mockServerURL, _ := url.Parse(mockServer.URL)
  2892  	cfg := store.Config{
  2893  		StoreBaseURL: mockServerURL,
  2894  	}
  2895  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2896  	sto := store.New(&cfg, dauthCtx)
  2897  
  2898  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2899  		{
  2900  			InstanceName:    "hello-world",
  2901  			SnapID:          helloWorldSnapID,
  2902  			TrackingChannel: "stable",
  2903  			Revision:        snap.R(26),
  2904  			RefreshedDate:   helloRefreshedDate,
  2905  		},
  2906  	}, []*store.SnapAction{
  2907  		{
  2908  			Action:       "refresh",
  2909  			SnapID:       helloWorldSnapID,
  2910  			Channel:      "stable",
  2911  			InstanceName: "hello-world",
  2912  		},
  2913  	}, nil, nil, nil)
  2914  	c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected refresh`)
  2915  	c.Assert(results, IsNil)
  2916  }
  2917  
  2918  func (s *storeActionSuite) TestSnapActionUnexpectedErrorKey(c *C) {
  2919  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2920  		assertRequest(c, r, "POST", snapActionPath)
  2921  		// check device authorization is set, implicitly checking doRequest was used
  2922  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2923  
  2924  		jsonReq, err := ioutil.ReadAll(r.Body)
  2925  		c.Assert(err, IsNil)
  2926  		var req struct {
  2927  			Context []map[string]interface{} `json:"context"`
  2928  			Actions []map[string]interface{} `json:"actions"`
  2929  		}
  2930  
  2931  		err = json.Unmarshal(jsonReq, &req)
  2932  		c.Assert(err, IsNil)
  2933  
  2934  		c.Assert(req.Context, HasLen, 2)
  2935  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2936  			"snap-id":          helloWorldSnapID,
  2937  			"instance-key":     helloWorldSnapID,
  2938  			"revision":         float64(26),
  2939  			"tracking-channel": "stable",
  2940  			"refreshed-date":   helloRefreshedDateStr,
  2941  			"epoch":            iZeroEpoch,
  2942  		})
  2943  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2944  			"snap-id":          helloWorldSnapID,
  2945  			"instance-key":     helloWorldFooInstanceKeyWithSalt,
  2946  			"revision":         float64(2),
  2947  			"tracking-channel": "stable",
  2948  			"refreshed-date":   helloRefreshedDateStr,
  2949  			"epoch":            iZeroEpoch,
  2950  		})
  2951  		c.Assert(req.Actions, HasLen, 1)
  2952  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2953  			"action":       "install",
  2954  			"instance-key": "install-1",
  2955  			"name":         "foo-2",
  2956  			"epoch":        nil,
  2957  		})
  2958  
  2959  		io.WriteString(w, `{
  2960    "results": [{
  2961       "result": "install",
  2962       "instance-key": "install-1",
  2963       "snap-id": "foo-2-id",
  2964       "name": "foo-2",
  2965       "snap": {
  2966         "snap-id": "foo-2-id",
  2967         "name": "foo-2",
  2968         "revision": 28,
  2969         "version": "6.1",
  2970         "publisher": {
  2971            "id": "canonical",
  2972            "username": "canonical",
  2973            "display-name": "Canonical"
  2974         }
  2975       }
  2976    },{
  2977        "error": {
  2978          "code": "duplicated-snap",
  2979           "message": "The Snap is present more than once in the request."
  2980        },
  2981        "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk",
  2982        "name": null,
  2983        "result": "error",
  2984        "snap": null,
  2985        "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
  2986    }]
  2987  }`)
  2988  	}))
  2989  
  2990  	c.Assert(mockServer, NotNil)
  2991  	defer mockServer.Close()
  2992  
  2993  	mockServerURL, _ := url.Parse(mockServer.URL)
  2994  	cfg := store.Config{
  2995  		StoreBaseURL: mockServerURL,
  2996  	}
  2997  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2998  	sto := store.New(&cfg, dauthCtx)
  2999  
  3000  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  3001  		{
  3002  			InstanceName:    "hello-world",
  3003  			SnapID:          helloWorldSnapID,
  3004  			TrackingChannel: "stable",
  3005  			Revision:        snap.R(26),
  3006  			RefreshedDate:   helloRefreshedDate,
  3007  		}, {
  3008  			InstanceName:    "hello-world_foo",
  3009  			SnapID:          helloWorldSnapID,
  3010  			TrackingChannel: "stable",
  3011  			Revision:        snap.R(2),
  3012  			RefreshedDate:   helloRefreshedDate,
  3013  		},
  3014  	}, []*store.SnapAction{
  3015  		{
  3016  			Action:       "install",
  3017  			InstanceName: "foo-2",
  3018  		},
  3019  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  3020  	c.Assert(err, DeepEquals, &store.SnapActionError{
  3021  		Other: []error{fmt.Errorf(`snap "hello-world_foo": The Snap is present more than once in the request.`)},
  3022  	})
  3023  	c.Assert(results, HasLen, 1)
  3024  	c.Assert(results[0].InstanceName(), Equals, "foo-2")
  3025  	c.Assert(results[0].SnapID, Equals, "foo-2-id")
  3026  }