github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/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) TestSnapActionInstallWithValidationSets(c *C) {
   757  	s.testSnapActionGet("install", "", "", [][]string{{"foo", "bar"}, {"foo", "baz"}}, c)
   758  }
   759  
   760  func (s *storeActionSuite) TestSnapActionAutoRefresh(c *C) {
   761  	// the bare TestSnapAction does more SnapAction checks; look there
   762  	// this one mostly just checks the refresh-reason header
   763  
   764  	restore := release.MockOnClassic(false)
   765  	defer restore()
   766  
   767  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   768  		assertRequest(c, r, "POST", snapActionPath)
   769  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled")
   770  
   771  		io.WriteString(w, `{
   772    "results": [{
   773       "result": "refresh",
   774       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   775       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   776       "name": "hello-world",
   777       "snap": {
   778         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   779         "name": "hello-world",
   780         "revision": 26,
   781         "version": "6.1",
   782         "epoch": {"read": [0], "write": [0]},
   783         "publisher": {
   784            "id": "canonical",
   785            "username": "canonical",
   786            "display-name": "Canonical"
   787         }
   788       }
   789    }]
   790  }`)
   791  	}))
   792  
   793  	c.Assert(mockServer, NotNil)
   794  	defer mockServer.Close()
   795  
   796  	mockServerURL, _ := url.Parse(mockServer.URL)
   797  	cfg := store.Config{
   798  		StoreBaseURL: mockServerURL,
   799  	}
   800  	dauthCtx := &testDauthContext{c: c, device: s.device}
   801  	sto := store.New(&cfg, dauthCtx)
   802  
   803  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   804  		{
   805  			InstanceName:    "hello-world",
   806  			SnapID:          helloWorldSnapID,
   807  			TrackingChannel: "beta",
   808  			Revision:        snap.R(1),
   809  			RefreshedDate:   helloRefreshedDate,
   810  		},
   811  	}, []*store.SnapAction{
   812  		{
   813  			Action:       "refresh",
   814  			SnapID:       helloWorldSnapID,
   815  			InstanceName: "hello-world",
   816  		},
   817  	}, nil, nil, &store.RefreshOptions{IsAutoRefresh: true})
   818  	c.Assert(err, IsNil)
   819  	c.Assert(results, HasLen, 1)
   820  }
   821  
   822  func (s *storeActionSuite) TestInstallFallbackChannelIsStable(c *C) {
   823  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   824  		assertRequest(c, r, "POST", snapActionPath)
   825  		// check device authorization is set, implicitly checking doRequest was used
   826  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   827  
   828  		jsonReq, err := ioutil.ReadAll(r.Body)
   829  		c.Assert(err, IsNil)
   830  		var req struct {
   831  			Context []map[string]interface{} `json:"context"`
   832  			Actions []map[string]interface{} `json:"actions"`
   833  		}
   834  
   835  		err = json.Unmarshal(jsonReq, &req)
   836  		c.Assert(err, IsNil)
   837  
   838  		c.Assert(req.Context, HasLen, 1)
   839  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   840  			"snap-id":          helloWorldSnapID,
   841  			"instance-key":     helloWorldSnapID,
   842  			"revision":         float64(1),
   843  			"tracking-channel": "stable",
   844  			"refreshed-date":   helloRefreshedDateStr,
   845  			"epoch":            iZeroEpoch,
   846  		})
   847  		c.Assert(req.Actions, HasLen, 1)
   848  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   849  			"action":       "refresh",
   850  			"instance-key": helloWorldSnapID,
   851  			"snap-id":      helloWorldSnapID,
   852  		})
   853  
   854  		io.WriteString(w, `{
   855    "results": [{
   856       "result": "refresh",
   857       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   858       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   859       "name": "hello-world",
   860       "snap": {
   861         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   862         "name": "hello-world",
   863         "revision": 26,
   864         "version": "6.1",
   865         "publisher": {
   866            "id": "canonical",
   867            "username": "canonical",
   868            "display-name": "Canonical"
   869         }
   870       }
   871    }]
   872  }`)
   873  	}))
   874  
   875  	c.Assert(mockServer, NotNil)
   876  	defer mockServer.Close()
   877  
   878  	mockServerURL, _ := url.Parse(mockServer.URL)
   879  	cfg := store.Config{
   880  		StoreBaseURL: mockServerURL,
   881  	}
   882  	dauthCtx := &testDauthContext{c: c, device: s.device}
   883  	sto := store.New(&cfg, dauthCtx)
   884  
   885  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   886  		{
   887  			InstanceName:  "hello-world",
   888  			SnapID:        helloWorldSnapID,
   889  			RefreshedDate: helloRefreshedDate,
   890  			Revision:      snap.R(1),
   891  		},
   892  	}, []*store.SnapAction{
   893  		{
   894  			Action:       "refresh",
   895  			SnapID:       helloWorldSnapID,
   896  			InstanceName: "hello-world",
   897  		},
   898  	}, nil, nil, nil)
   899  	c.Assert(err, IsNil)
   900  	c.Assert(results, HasLen, 1)
   901  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   902  	c.Assert(results[0].Revision, Equals, snap.R(26))
   903  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
   904  }
   905  
   906  func (s *storeActionSuite) TestSnapActionNonDefaultsHeaders(c *C) {
   907  	restore := release.MockOnClassic(true)
   908  	defer restore()
   909  
   910  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   911  		assertRequest(c, r, "POST", snapActionPath)
   912  		// check device authorization is set, implicitly checking doRequest was used
   913  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
   914  
   915  		storeID := r.Header.Get("Snap-Device-Store")
   916  		c.Check(storeID, Equals, "foo")
   917  
   918  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, "21")
   919  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, "archXYZ")
   920  		c.Check(r.Header.Get("Snap-Classic"), Equals, "true")
   921  
   922  		jsonReq, err := ioutil.ReadAll(r.Body)
   923  		c.Assert(err, IsNil)
   924  		var req struct {
   925  			Context []map[string]interface{} `json:"context"`
   926  			Actions []map[string]interface{} `json:"actions"`
   927  		}
   928  
   929  		err = json.Unmarshal(jsonReq, &req)
   930  		c.Assert(err, IsNil)
   931  
   932  		c.Assert(req.Context, HasLen, 1)
   933  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
   934  			"snap-id":          helloWorldSnapID,
   935  			"instance-key":     helloWorldSnapID,
   936  			"revision":         float64(1),
   937  			"tracking-channel": "beta",
   938  			"refreshed-date":   helloRefreshedDateStr,
   939  			"epoch":            iZeroEpoch,
   940  		})
   941  		c.Assert(req.Actions, HasLen, 1)
   942  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
   943  			"action":       "refresh",
   944  			"instance-key": helloWorldSnapID,
   945  			"snap-id":      helloWorldSnapID,
   946  		})
   947  
   948  		io.WriteString(w, `{
   949    "results": [{
   950       "result": "refresh",
   951       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   952       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   953       "name": "hello-world",
   954       "snap": {
   955         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
   956         "name": "hello-world",
   957         "revision": 26,
   958         "version": "6.1",
   959         "publisher": {
   960            "id": "canonical",
   961            "username": "canonical",
   962            "display-name": "Canonical"
   963         }
   964       }
   965    }]
   966  }`)
   967  	}))
   968  
   969  	c.Assert(mockServer, NotNil)
   970  	defer mockServer.Close()
   971  
   972  	mockServerURL, _ := url.Parse(mockServer.URL)
   973  	cfg := store.DefaultConfig()
   974  	cfg.StoreBaseURL = mockServerURL
   975  	cfg.Series = "21"
   976  	cfg.Architecture = "archXYZ"
   977  	cfg.StoreID = "foo"
   978  	dauthCtx := &testDauthContext{c: c, device: s.device}
   979  	sto := store.New(cfg, dauthCtx)
   980  
   981  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
   982  		{
   983  			InstanceName:    "hello-world",
   984  			SnapID:          helloWorldSnapID,
   985  			TrackingChannel: "beta",
   986  			RefreshedDate:   helloRefreshedDate,
   987  			Revision:        snap.R(1),
   988  		},
   989  	}, []*store.SnapAction{
   990  		{
   991  			Action:       "refresh",
   992  			SnapID:       helloWorldSnapID,
   993  			InstanceName: "hello-world",
   994  		},
   995  	}, nil, nil, nil)
   996  	c.Assert(err, IsNil)
   997  	c.Assert(results, HasLen, 1)
   998  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
   999  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1000  	c.Assert(results[0].Version, Equals, "6.1")
  1001  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1002  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1003  	c.Assert(results[0].Deltas, HasLen, 0)
  1004  }
  1005  
  1006  func (s *storeActionSuite) TestSnapActionWithDeltas(c *C) {
  1007  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
  1008  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
  1009  	c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
  1010  
  1011  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1012  		assertRequest(c, r, "POST", snapActionPath)
  1013  		// check device authorization is set, implicitly checking doRequest was used
  1014  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1015  
  1016  		c.Check(r.Header.Get("Snap-Accept-Delta-Format"), Equals, "xdelta3")
  1017  		jsonReq, err := ioutil.ReadAll(r.Body)
  1018  		c.Assert(err, IsNil)
  1019  		var req struct {
  1020  			Context []map[string]interface{} `json:"context"`
  1021  			Actions []map[string]interface{} `json:"actions"`
  1022  		}
  1023  
  1024  		err = json.Unmarshal(jsonReq, &req)
  1025  		c.Assert(err, IsNil)
  1026  
  1027  		c.Assert(req.Context, HasLen, 1)
  1028  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1029  			"snap-id":          helloWorldSnapID,
  1030  			"instance-key":     helloWorldSnapID,
  1031  			"revision":         float64(1),
  1032  			"tracking-channel": "beta",
  1033  			"refreshed-date":   helloRefreshedDateStr,
  1034  			"epoch":            iZeroEpoch,
  1035  		})
  1036  		c.Assert(req.Actions, HasLen, 1)
  1037  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1038  			"action":       "refresh",
  1039  			"instance-key": helloWorldSnapID,
  1040  			"snap-id":      helloWorldSnapID,
  1041  		})
  1042  
  1043  		io.WriteString(w, `{
  1044    "results": [{
  1045       "result": "refresh",
  1046       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1047       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1048       "name": "hello-world",
  1049       "snap": {
  1050         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1051         "name": "hello-world",
  1052         "revision": 26,
  1053         "version": "6.1",
  1054         "publisher": {
  1055            "id": "canonical",
  1056            "username": "canonical",
  1057            "display-name": "Canonical"
  1058         }
  1059       }
  1060    }]
  1061  }`)
  1062  	}))
  1063  
  1064  	c.Assert(mockServer, NotNil)
  1065  	defer mockServer.Close()
  1066  
  1067  	mockServerURL, _ := url.Parse(mockServer.URL)
  1068  	cfg := store.Config{
  1069  		StoreBaseURL: mockServerURL,
  1070  	}
  1071  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1072  	sto := store.New(&cfg, dauthCtx)
  1073  
  1074  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1075  		{
  1076  			InstanceName:    "hello-world",
  1077  			SnapID:          helloWorldSnapID,
  1078  			TrackingChannel: "beta",
  1079  			Revision:        snap.R(1),
  1080  			RefreshedDate:   helloRefreshedDate,
  1081  		},
  1082  	}, []*store.SnapAction{
  1083  		{
  1084  			Action:       "refresh",
  1085  			SnapID:       helloWorldSnapID,
  1086  			InstanceName: "hello-world",
  1087  		},
  1088  	}, nil, nil, nil)
  1089  	c.Assert(err, IsNil)
  1090  	c.Assert(results, HasLen, 1)
  1091  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1092  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1093  }
  1094  
  1095  func (s *storeActionSuite) TestSnapActionOptions(c *C) {
  1096  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1097  		assertRequest(c, r, "POST", snapActionPath)
  1098  		// check device authorization is set, implicitly checking doRequest was used
  1099  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1100  
  1101  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "true")
  1102  
  1103  		jsonReq, err := ioutil.ReadAll(r.Body)
  1104  		c.Assert(err, IsNil)
  1105  		var req struct {
  1106  			Context []map[string]interface{} `json:"context"`
  1107  			Actions []map[string]interface{} `json:"actions"`
  1108  		}
  1109  
  1110  		err = json.Unmarshal(jsonReq, &req)
  1111  		c.Assert(err, IsNil)
  1112  
  1113  		c.Assert(req.Context, HasLen, 1)
  1114  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1115  			"snap-id":          helloWorldSnapID,
  1116  			"instance-key":     helloWorldSnapID,
  1117  			"revision":         float64(1),
  1118  			"tracking-channel": "stable",
  1119  			"refreshed-date":   helloRefreshedDateStr,
  1120  			"epoch":            iZeroEpoch,
  1121  		})
  1122  		c.Assert(req.Actions, HasLen, 1)
  1123  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1124  			"action":       "refresh",
  1125  			"instance-key": helloWorldSnapID,
  1126  			"snap-id":      helloWorldSnapID,
  1127  			"channel":      "stable",
  1128  		})
  1129  
  1130  		io.WriteString(w, `{
  1131    "results": [{
  1132       "result": "refresh",
  1133       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1134       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1135       "name": "hello-world",
  1136       "snap": {
  1137         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1138         "name": "hello-world",
  1139         "revision": 26,
  1140         "version": "6.1",
  1141         "publisher": {
  1142            "id": "canonical",
  1143            "username": "canonical",
  1144            "display-name": "Canonical"
  1145         }
  1146       }
  1147    }]
  1148  }`)
  1149  	}))
  1150  
  1151  	c.Assert(mockServer, NotNil)
  1152  	defer mockServer.Close()
  1153  
  1154  	mockServerURL, _ := url.Parse(mockServer.URL)
  1155  	cfg := store.Config{
  1156  		StoreBaseURL: mockServerURL,
  1157  	}
  1158  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1159  	sto := store.New(&cfg, dauthCtx)
  1160  
  1161  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1162  		{
  1163  			InstanceName:    "hello-world",
  1164  			SnapID:          helloWorldSnapID,
  1165  			TrackingChannel: "stable",
  1166  			Revision:        snap.R(1),
  1167  			RefreshedDate:   helloRefreshedDate,
  1168  		},
  1169  	}, []*store.SnapAction{
  1170  		{
  1171  			Action:       "refresh",
  1172  			SnapID:       helloWorldSnapID,
  1173  			InstanceName: "hello-world",
  1174  			Channel:      "stable",
  1175  		},
  1176  	}, nil, nil, &store.RefreshOptions{RefreshManaged: true})
  1177  	c.Assert(err, IsNil)
  1178  	c.Assert(results, HasLen, 1)
  1179  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1180  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1181  }
  1182  
  1183  func (s *storeActionSuite) TestSnapActionInstall(c *C) {
  1184  	s.testSnapActionGet("install", "", "", nil, c)
  1185  }
  1186  func (s *storeActionSuite) TestSnapActionInstallWithCohort(c *C) {
  1187  	s.testSnapActionGet("install", "what", "", nil, c)
  1188  }
  1189  func (s *storeActionSuite) TestSnapActionDownload(c *C) {
  1190  	s.testSnapActionGet("download", "", "", nil, c)
  1191  }
  1192  func (s *storeActionSuite) TestSnapActionDownloadWithCohort(c *C) {
  1193  	s.testSnapActionGet("download", "here", "", nil, c)
  1194  }
  1195  func (s *storeActionSuite) TestSnapActionInstallRedirect(c *C) {
  1196  	s.testSnapActionGet("install", "", "2.0/candidate", nil, c)
  1197  }
  1198  func (s *storeActionSuite) TestSnapActionDownloadRedirect(c *C) {
  1199  	s.testSnapActionGet("download", "", "2.0/candidate", nil, c)
  1200  }
  1201  func (s *storeActionSuite) testSnapActionGet(action, cohort, redirectChannel string, validationSets [][]string, c *C) {
  1202  	// action here is one of install or download
  1203  	restore := release.MockOnClassic(false)
  1204  	defer restore()
  1205  
  1206  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1207  		assertRequest(c, r, "POST", snapActionPath)
  1208  		// check device authorization is set, implicitly checking doRequest was used
  1209  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1210  
  1211  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
  1212  
  1213  		// no store ID by default
  1214  		storeID := r.Header.Get("Snap-Device-Store")
  1215  		c.Check(storeID, Equals, "")
  1216  
  1217  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
  1218  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
  1219  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
  1220  
  1221  		jsonReq, err := ioutil.ReadAll(r.Body)
  1222  		c.Assert(err, IsNil)
  1223  		var req struct {
  1224  			Context []map[string]interface{} `json:"context"`
  1225  			Actions []map[string]interface{} `json:"actions"`
  1226  		}
  1227  
  1228  		err = json.Unmarshal(jsonReq, &req)
  1229  		c.Assert(err, IsNil)
  1230  
  1231  		c.Assert(req.Context, HasLen, 0)
  1232  		c.Assert(req.Actions, HasLen, 1)
  1233  		expectedAction := map[string]interface{}{
  1234  			"action":       action,
  1235  			"instance-key": action + "-1",
  1236  			"name":         "hello-world",
  1237  			"channel":      "beta",
  1238  			"epoch":        nil,
  1239  		}
  1240  		if cohort != "" {
  1241  			expectedAction["cohort-key"] = cohort
  1242  		}
  1243  		if validationSets != nil {
  1244  			// XXX: rewrite as otherwise DeepEquals complains about
  1245  			// []interface {}{[]interface {}{..} vs expected [][]string{[]string{..}.
  1246  			var sets []interface{}
  1247  			for _, vs := range validationSets {
  1248  				var vss []interface{}
  1249  				for _, vv := range vs {
  1250  					vss = append(vss, vv)
  1251  				}
  1252  				sets = append(sets, vss)
  1253  			}
  1254  			expectedAction["validation-sets"] = sets
  1255  		}
  1256  		c.Assert(req.Actions[0], DeepEquals, expectedAction)
  1257  
  1258  		fmt.Fprintf(w, `{
  1259    "results": [{
  1260       "result": "%s",
  1261       "instance-key": "%[1]s-1",
  1262       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1263       "name": "hello-world",
  1264       "effective-channel": "candidate",
  1265       "redirect-channel": "%s",
  1266       "snap": {
  1267         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1268         "name": "hello-world",
  1269         "revision": 26,
  1270         "version": "6.1",
  1271         "publisher": {
  1272            "id": "canonical",
  1273            "username": "canonical",
  1274            "display-name": "Canonical"
  1275         }
  1276       }
  1277    }]
  1278  }`, action, redirectChannel)
  1279  	}))
  1280  
  1281  	c.Assert(mockServer, NotNil)
  1282  	defer mockServer.Close()
  1283  
  1284  	mockServerURL, _ := url.Parse(mockServer.URL)
  1285  	cfg := store.Config{
  1286  		StoreBaseURL: mockServerURL,
  1287  	}
  1288  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1289  	sto := store.New(&cfg, dauthCtx)
  1290  
  1291  	results, _, err := sto.SnapAction(s.ctx, nil,
  1292  		[]*store.SnapAction{
  1293  			{
  1294  				Action:         action,
  1295  				InstanceName:   "hello-world",
  1296  				Channel:        "beta",
  1297  				CohortKey:      cohort,
  1298  				ValidationSets: validationSets,
  1299  			},
  1300  		}, nil, nil, nil)
  1301  	c.Assert(err, IsNil)
  1302  	c.Assert(results, HasLen, 1)
  1303  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1304  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1305  	c.Assert(results[0].Version, Equals, "6.1")
  1306  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1307  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1308  	c.Assert(results[0].Deltas, HasLen, 0)
  1309  	// effective-channel
  1310  	c.Assert(results[0].Channel, Equals, "candidate")
  1311  	c.Assert(results[0].RedirectChannel, Equals, redirectChannel)
  1312  }
  1313  
  1314  func (s *storeActionSuite) TestSnapActionInstallAmend(c *C) {
  1315  	// this is what amend would look like
  1316  	restore := release.MockOnClassic(false)
  1317  	defer restore()
  1318  
  1319  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1320  		assertRequest(c, r, "POST", snapActionPath)
  1321  		// check device authorization is set, implicitly checking doRequest was used
  1322  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1323  
  1324  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
  1325  
  1326  		// no store ID by default
  1327  		storeID := r.Header.Get("Snap-Device-Store")
  1328  		c.Check(storeID, Equals, "")
  1329  
  1330  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
  1331  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
  1332  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
  1333  
  1334  		jsonReq, err := ioutil.ReadAll(r.Body)
  1335  		c.Assert(err, IsNil)
  1336  		var req struct {
  1337  			Context []map[string]interface{} `json:"context"`
  1338  			Actions []map[string]interface{} `json:"actions"`
  1339  		}
  1340  
  1341  		err = json.Unmarshal(jsonReq, &req)
  1342  		c.Assert(err, IsNil)
  1343  
  1344  		c.Assert(req.Context, HasLen, 0)
  1345  		c.Assert(req.Actions, HasLen, 1)
  1346  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1347  			"action":       "install",
  1348  			"instance-key": "install-1",
  1349  			"name":         "hello-world",
  1350  			"channel":      "beta",
  1351  			"epoch":        map[string]interface{}{"read": []interface{}{0., 1.}, "write": []interface{}{1.}},
  1352  		})
  1353  
  1354  		fmt.Fprint(w, `{
  1355    "results": [{
  1356       "result": "install",
  1357       "instance-key": "install-1",
  1358       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1359       "name": "hello-world",
  1360       "effective-channel": "candidate",
  1361       "snap": {
  1362         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1363         "name": "hello-world",
  1364         "revision": 26,
  1365         "version": "6.1",
  1366         "publisher": {
  1367            "id": "canonical",
  1368            "username": "canonical",
  1369            "display-name": "Canonical"
  1370         }
  1371       }
  1372    }]
  1373  }`)
  1374  	}))
  1375  
  1376  	c.Assert(mockServer, NotNil)
  1377  	defer mockServer.Close()
  1378  
  1379  	mockServerURL, _ := url.Parse(mockServer.URL)
  1380  	cfg := store.Config{
  1381  		StoreBaseURL: mockServerURL,
  1382  	}
  1383  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1384  	sto := store.New(&cfg, dauthCtx)
  1385  
  1386  	results, _, err := sto.SnapAction(s.ctx, nil,
  1387  		[]*store.SnapAction{
  1388  			{
  1389  				Action:       "install",
  1390  				InstanceName: "hello-world",
  1391  				Channel:      "beta",
  1392  				Epoch:        snap.E("1*"),
  1393  			},
  1394  		}, nil, nil, nil)
  1395  	c.Assert(err, IsNil)
  1396  	c.Assert(results, HasLen, 1)
  1397  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1398  	c.Assert(results[0].Revision, Equals, snap.R(26))
  1399  	c.Assert(results[0].Version, Equals, "6.1")
  1400  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1401  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1402  	c.Assert(results[0].Deltas, HasLen, 0)
  1403  	// effective-channel
  1404  	c.Assert(results[0].Channel, Equals, "candidate")
  1405  }
  1406  
  1407  func (s *storeActionSuite) TestSnapActionWithClientUserAgent(c *C) {
  1408  	restore := release.MockOnClassic(false)
  1409  	defer restore()
  1410  
  1411  	serverCalls := 0
  1412  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1413  		serverCalls++
  1414  		assertRequest(c, r, "POST", snapActionPath)
  1415  
  1416  		c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, "some-snap-agent/1.0")
  1417  
  1418  		io.WriteString(w, `{
  1419    "results": []
  1420  }`)
  1421  	}))
  1422  
  1423  	c.Assert(mockServer, NotNil)
  1424  	defer mockServer.Close()
  1425  
  1426  	mockServerURL, _ := url.Parse(mockServer.URL)
  1427  	cfg := store.Config{
  1428  		StoreBaseURL: mockServerURL,
  1429  	}
  1430  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1431  	sto := store.New(&cfg, dauthCtx)
  1432  
  1433  	// to construct the client-user-agent context we need to
  1434  	// create a req that simulates what the req that the daemon got
  1435  	r, err := http.NewRequest("POST", "/snapd/api", nil)
  1436  	r.Header.Set("User-Agent", "some-snap-agent/1.0")
  1437  	c.Assert(err, IsNil)
  1438  	ctx := store.WithClientUserAgent(s.ctx, r)
  1439  
  1440  	results, _, err := sto.SnapAction(ctx, nil, []*store.SnapAction{{Action: "install", InstanceName: "some-snap"}}, nil, nil, nil)
  1441  	c.Check(serverCalls, Equals, 1)
  1442  	c.Check(results, HasLen, 0)
  1443  	c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true})
  1444  }
  1445  
  1446  func (s *storeActionSuite) TestSnapActionDownloadParallelInstanceKey(c *C) {
  1447  	// action here is one of install or download
  1448  	restore := release.MockOnClassic(false)
  1449  	defer restore()
  1450  
  1451  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1452  		c.Fatal("should not be reached")
  1453  	}))
  1454  
  1455  	c.Assert(mockServer, NotNil)
  1456  	defer mockServer.Close()
  1457  
  1458  	mockServerURL, _ := url.Parse(mockServer.URL)
  1459  	cfg := store.Config{
  1460  		StoreBaseURL: mockServerURL,
  1461  	}
  1462  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1463  	sto := store.New(&cfg, dauthCtx)
  1464  
  1465  	_, _, err := sto.SnapAction(s.ctx, nil,
  1466  		[]*store.SnapAction{
  1467  			{
  1468  				Action:       "download",
  1469  				InstanceName: "hello-world_foo",
  1470  				Channel:      "beta",
  1471  			},
  1472  		}, nil, nil, nil)
  1473  	c.Assert(err, ErrorMatches, `internal error: unsupported download with instance name "hello-world_foo"`)
  1474  }
  1475  
  1476  func (s *storeActionSuite) TestSnapActionInstallWithRevision(c *C) {
  1477  	s.testSnapActionGetWithRevision("install", c)
  1478  }
  1479  
  1480  func (s *storeActionSuite) TestSnapActionDownloadWithRevision(c *C) {
  1481  	s.testSnapActionGetWithRevision("download", c)
  1482  }
  1483  
  1484  func (s *storeActionSuite) testSnapActionGetWithRevision(action string, c *C) {
  1485  	// action here is one of install or download
  1486  	restore := release.MockOnClassic(false)
  1487  	defer restore()
  1488  
  1489  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1490  		assertRequest(c, r, "POST", snapActionPath)
  1491  		// check device authorization is set, implicitly checking doRequest was used
  1492  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1493  
  1494  		c.Check(r.Header.Get("Snap-Refresh-Managed"), Equals, "")
  1495  
  1496  		// no store ID by default
  1497  		storeID := r.Header.Get("Snap-Device-Store")
  1498  		c.Check(storeID, Equals, "")
  1499  
  1500  		c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
  1501  		c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
  1502  		c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
  1503  
  1504  		jsonReq, err := ioutil.ReadAll(r.Body)
  1505  		c.Assert(err, IsNil)
  1506  		var req struct {
  1507  			Context []map[string]interface{} `json:"context"`
  1508  			Actions []map[string]interface{} `json:"actions"`
  1509  		}
  1510  
  1511  		err = json.Unmarshal(jsonReq, &req)
  1512  		c.Assert(err, IsNil)
  1513  
  1514  		c.Assert(req.Context, HasLen, 0)
  1515  		c.Assert(req.Actions, HasLen, 1)
  1516  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1517  			"action":       action,
  1518  			"instance-key": action + "-1",
  1519  			"name":         "hello-world",
  1520  			"revision":     float64(28),
  1521  			"epoch":        nil,
  1522  		})
  1523  
  1524  		fmt.Fprintf(w, `{
  1525    "results": [{
  1526       "result": "%s",
  1527       "instance-key": "%[1]s-1",
  1528       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1529       "name": "hello-world",
  1530       "snap": {
  1531         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1532         "name": "hello-world",
  1533         "revision": 28,
  1534         "version": "6.1",
  1535         "publisher": {
  1536            "id": "canonical",
  1537            "username": "canonical",
  1538            "display-name": "Canonical"
  1539         }
  1540       }
  1541    }]
  1542  }`, action)
  1543  	}))
  1544  
  1545  	c.Assert(mockServer, NotNil)
  1546  	defer mockServer.Close()
  1547  
  1548  	mockServerURL, _ := url.Parse(mockServer.URL)
  1549  	cfg := store.Config{
  1550  		StoreBaseURL: mockServerURL,
  1551  	}
  1552  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1553  	sto := store.New(&cfg, dauthCtx)
  1554  
  1555  	results, _, err := sto.SnapAction(s.ctx, nil,
  1556  		[]*store.SnapAction{
  1557  			{
  1558  				Action:       action,
  1559  				InstanceName: "hello-world",
  1560  				Revision:     snap.R(28),
  1561  			},
  1562  		}, nil, nil, nil)
  1563  	c.Assert(err, IsNil)
  1564  	c.Assert(results, HasLen, 1)
  1565  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  1566  	c.Assert(results[0].Revision, Equals, snap.R(28))
  1567  	c.Assert(results[0].Version, Equals, "6.1")
  1568  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  1569  	c.Assert(results[0].Publisher.ID, Equals, helloWorldDeveloperID)
  1570  	c.Assert(results[0].Deltas, HasLen, 0)
  1571  	// effective-channel is not set
  1572  	c.Assert(results[0].Channel, Equals, "")
  1573  }
  1574  
  1575  func (s *storeActionSuite) TestSnapActionRevisionNotAvailable(c *C) {
  1576  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1577  		assertRequest(c, r, "POST", snapActionPath)
  1578  		// check device authorization is set, implicitly checking doRequest was used
  1579  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1580  
  1581  		jsonReq, err := ioutil.ReadAll(r.Body)
  1582  		c.Assert(err, IsNil)
  1583  		var req struct {
  1584  			Context []map[string]interface{} `json:"context"`
  1585  			Actions []map[string]interface{} `json:"actions"`
  1586  		}
  1587  
  1588  		err = json.Unmarshal(jsonReq, &req)
  1589  		c.Assert(err, IsNil)
  1590  
  1591  		c.Assert(req.Context, HasLen, 2)
  1592  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1593  			"snap-id":          helloWorldSnapID,
  1594  			"instance-key":     helloWorldSnapID,
  1595  			"revision":         float64(26),
  1596  			"tracking-channel": "stable",
  1597  			"refreshed-date":   helloRefreshedDateStr,
  1598  			"epoch":            iZeroEpoch,
  1599  		})
  1600  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  1601  			"snap-id":          "snap2-id",
  1602  			"instance-key":     "snap2-id",
  1603  			"revision":         float64(2),
  1604  			"tracking-channel": "edge",
  1605  			"refreshed-date":   helloRefreshedDateStr,
  1606  			"epoch":            iZeroEpoch,
  1607  		})
  1608  		c.Assert(req.Actions, HasLen, 4)
  1609  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1610  			"action":       "refresh",
  1611  			"instance-key": helloWorldSnapID,
  1612  			"snap-id":      helloWorldSnapID,
  1613  		})
  1614  		c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{
  1615  			"action":       "refresh",
  1616  			"instance-key": "snap2-id",
  1617  			"snap-id":      "snap2-id",
  1618  			"channel":      "candidate",
  1619  		})
  1620  		c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{
  1621  			"action":       "install",
  1622  			"instance-key": "install-1",
  1623  			"name":         "foo",
  1624  			"channel":      "stable",
  1625  			"epoch":        nil,
  1626  		})
  1627  		c.Assert(req.Actions[3], DeepEquals, map[string]interface{}{
  1628  			"action":       "download",
  1629  			"instance-key": "download-1",
  1630  			"name":         "bar",
  1631  			"revision":     42.,
  1632  			"epoch":        nil,
  1633  		})
  1634  
  1635  		io.WriteString(w, `{
  1636    "results": [{
  1637       "result": "error",
  1638       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1639       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1640       "name": "hello-world",
  1641       "error": {
  1642         "code": "revision-not-found",
  1643         "message": "msg1"
  1644       }
  1645    }, {
  1646       "result": "error",
  1647       "instance-key": "snap2-id",
  1648       "snap-id": "snap2-id",
  1649       "name": "snap2",
  1650       "error": {
  1651         "code": "revision-not-found",
  1652         "message": "msg1",
  1653         "extra": {
  1654           "releases": [{"architecture": "amd64", "channel": "beta"},
  1655                        {"architecture": "arm64", "channel": "beta"}]
  1656         }
  1657       }
  1658    }, {
  1659       "result": "error",
  1660       "instance-key": "install-1",
  1661       "snap-id": "foo-id",
  1662       "name": "foo",
  1663       "error": {
  1664         "code": "revision-not-found",
  1665         "message": "msg2"
  1666       }
  1667    }, {
  1668       "result": "error",
  1669       "instance-key": "download-1",
  1670       "snap-id": "bar-id",
  1671       "name": "bar",
  1672       "error": {
  1673         "code": "revision-not-found",
  1674         "message": "msg3"
  1675       }
  1676    }]
  1677  }`)
  1678  	}))
  1679  
  1680  	c.Assert(mockServer, NotNil)
  1681  	defer mockServer.Close()
  1682  
  1683  	mockServerURL, _ := url.Parse(mockServer.URL)
  1684  	cfg := store.Config{
  1685  		StoreBaseURL: mockServerURL,
  1686  	}
  1687  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1688  	sto := store.New(&cfg, dauthCtx)
  1689  
  1690  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1691  		{
  1692  			InstanceName:    "hello-world",
  1693  			SnapID:          helloWorldSnapID,
  1694  			TrackingChannel: "stable",
  1695  			Revision:        snap.R(26),
  1696  			RefreshedDate:   helloRefreshedDate,
  1697  		},
  1698  		{
  1699  			InstanceName:    "snap2",
  1700  			SnapID:          "snap2-id",
  1701  			TrackingChannel: "edge",
  1702  			Revision:        snap.R(2),
  1703  			RefreshedDate:   helloRefreshedDate,
  1704  		},
  1705  	}, []*store.SnapAction{
  1706  		{
  1707  			Action:       "refresh",
  1708  			InstanceName: "hello-world",
  1709  			SnapID:       helloWorldSnapID,
  1710  		}, {
  1711  			Action:       "refresh",
  1712  			InstanceName: "snap2",
  1713  			SnapID:       "snap2-id",
  1714  			Channel:      "candidate",
  1715  		}, {
  1716  			Action:       "install",
  1717  			InstanceName: "foo",
  1718  			Channel:      "stable",
  1719  		}, {
  1720  			Action:       "download",
  1721  			InstanceName: "bar",
  1722  			Revision:     snap.R(42),
  1723  		},
  1724  	}, nil, nil, nil)
  1725  	c.Assert(results, HasLen, 0)
  1726  	c.Check(err, DeepEquals, &store.SnapActionError{
  1727  		Refresh: map[string]error{
  1728  			"hello-world": &store.RevisionNotAvailableError{
  1729  				Action:  "refresh",
  1730  				Channel: "stable",
  1731  			},
  1732  			"snap2": &store.RevisionNotAvailableError{
  1733  				Action:  "refresh",
  1734  				Channel: "candidate",
  1735  				Releases: []channel.Channel{
  1736  					snaptest.MustParseChannel("beta", "amd64"),
  1737  					snaptest.MustParseChannel("beta", "arm64"),
  1738  				},
  1739  			},
  1740  		},
  1741  		Install: map[string]error{
  1742  			"foo": &store.RevisionNotAvailableError{
  1743  				Action:  "install",
  1744  				Channel: "stable",
  1745  			},
  1746  		},
  1747  		Download: map[string]error{
  1748  			"bar": &store.RevisionNotAvailableError{
  1749  				Action:  "download",
  1750  				Channel: "",
  1751  			},
  1752  		},
  1753  	})
  1754  }
  1755  
  1756  func (s *storeActionSuite) TestSnapActionSnapNotFound(c *C) {
  1757  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1758  		assertRequest(c, r, "POST", snapActionPath)
  1759  		// check device authorization is set, implicitly checking doRequest was used
  1760  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1761  
  1762  		jsonReq, err := ioutil.ReadAll(r.Body)
  1763  		c.Assert(err, IsNil)
  1764  		var req struct {
  1765  			Context []map[string]interface{} `json:"context"`
  1766  			Actions []map[string]interface{} `json:"actions"`
  1767  		}
  1768  
  1769  		err = json.Unmarshal(jsonReq, &req)
  1770  		c.Assert(err, IsNil)
  1771  
  1772  		c.Assert(req.Context, HasLen, 1)
  1773  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  1774  			"snap-id":          helloWorldSnapID,
  1775  			"instance-key":     helloWorldSnapID,
  1776  			"revision":         float64(26),
  1777  			"tracking-channel": "stable",
  1778  			"refreshed-date":   helloRefreshedDateStr,
  1779  			"epoch":            iZeroEpoch,
  1780  		})
  1781  		c.Assert(req.Actions, HasLen, 3)
  1782  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1783  			"action":       "refresh",
  1784  			"instance-key": helloWorldSnapID,
  1785  			"snap-id":      helloWorldSnapID,
  1786  			"channel":      "stable",
  1787  		})
  1788  		c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{
  1789  			"action":       "install",
  1790  			"instance-key": "install-1",
  1791  			"name":         "foo",
  1792  			"channel":      "stable",
  1793  			"epoch":        nil,
  1794  		})
  1795  		c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{
  1796  			"action":       "download",
  1797  			"instance-key": "download-1",
  1798  			"name":         "bar",
  1799  			"revision":     42.,
  1800  			"epoch":        nil,
  1801  		})
  1802  
  1803  		io.WriteString(w, `{
  1804    "results": [{
  1805       "result": "error",
  1806       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1807       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  1808       "error": {
  1809         "code": "id-not-found",
  1810         "message": "msg1"
  1811       }
  1812    }, {
  1813       "result": "error",
  1814       "instance-key": "install-1",
  1815       "name": "foo",
  1816       "error": {
  1817         "code": "name-not-found",
  1818         "message": "msg2"
  1819       }
  1820    }, {
  1821       "result": "error",
  1822       "instance-key": "download-1",
  1823       "name": "bar",
  1824       "error": {
  1825         "code": "name-not-found",
  1826         "message": "msg3"
  1827       }
  1828    }]
  1829  }`)
  1830  	}))
  1831  
  1832  	c.Assert(mockServer, NotNil)
  1833  	defer mockServer.Close()
  1834  
  1835  	mockServerURL, _ := url.Parse(mockServer.URL)
  1836  	cfg := store.Config{
  1837  		StoreBaseURL: mockServerURL,
  1838  	}
  1839  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1840  	sto := store.New(&cfg, dauthCtx)
  1841  
  1842  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  1843  		{
  1844  			InstanceName:    "hello-world",
  1845  			SnapID:          helloWorldSnapID,
  1846  			TrackingChannel: "stable",
  1847  			Revision:        snap.R(26),
  1848  			RefreshedDate:   helloRefreshedDate,
  1849  		},
  1850  	}, []*store.SnapAction{
  1851  		{
  1852  			Action:       "refresh",
  1853  			SnapID:       helloWorldSnapID,
  1854  			InstanceName: "hello-world",
  1855  			Channel:      "stable",
  1856  		}, {
  1857  			Action:       "install",
  1858  			InstanceName: "foo",
  1859  			Channel:      "stable",
  1860  		}, {
  1861  			Action:       "download",
  1862  			InstanceName: "bar",
  1863  			Revision:     snap.R(42),
  1864  		},
  1865  	}, nil, nil, nil)
  1866  	c.Assert(results, HasLen, 0)
  1867  	c.Check(err, DeepEquals, &store.SnapActionError{
  1868  		Refresh: map[string]error{
  1869  			"hello-world": store.ErrSnapNotFound,
  1870  		},
  1871  		Install: map[string]error{
  1872  			"foo": store.ErrSnapNotFound,
  1873  		},
  1874  		Download: map[string]error{
  1875  			"bar": store.ErrSnapNotFound,
  1876  		},
  1877  	})
  1878  }
  1879  
  1880  func (s *storeActionSuite) TestSnapActionOtherErrors(c *C) {
  1881  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1882  		assertRequest(c, r, "POST", snapActionPath)
  1883  		// check device authorization is set, implicitly checking doRequest was used
  1884  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  1885  
  1886  		jsonReq, err := ioutil.ReadAll(r.Body)
  1887  		c.Assert(err, IsNil)
  1888  		var req struct {
  1889  			Context []map[string]interface{} `json:"context"`
  1890  			Actions []map[string]interface{} `json:"actions"`
  1891  		}
  1892  
  1893  		err = json.Unmarshal(jsonReq, &req)
  1894  		c.Assert(err, IsNil)
  1895  
  1896  		c.Assert(req.Context, HasLen, 0)
  1897  		c.Assert(req.Actions, HasLen, 1)
  1898  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  1899  			"action":       "install",
  1900  			"instance-key": "install-1",
  1901  			"name":         "foo",
  1902  			"channel":      "stable",
  1903  			"epoch":        nil,
  1904  		})
  1905  
  1906  		io.WriteString(w, `{
  1907    "results": [{
  1908       "result": "error",
  1909       "error": {
  1910         "code": "other1",
  1911         "message": "other error one"
  1912       }
  1913    }],
  1914    "error-list": [
  1915       {"code": "global-error", "message": "global error"}
  1916    ]
  1917  }`)
  1918  	}))
  1919  
  1920  	c.Assert(mockServer, NotNil)
  1921  	defer mockServer.Close()
  1922  
  1923  	mockServerURL, _ := url.Parse(mockServer.URL)
  1924  	cfg := store.Config{
  1925  		StoreBaseURL: mockServerURL,
  1926  	}
  1927  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1928  	sto := store.New(&cfg, dauthCtx)
  1929  
  1930  	results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{
  1931  		{
  1932  			Action:       "install",
  1933  			InstanceName: "foo",
  1934  			Channel:      "stable",
  1935  		},
  1936  	}, nil, nil, nil)
  1937  	c.Assert(results, HasLen, 0)
  1938  	c.Check(err, DeepEquals, &store.SnapActionError{
  1939  		Other: []error{
  1940  			fmt.Errorf("other error one"),
  1941  			fmt.Errorf("global error"),
  1942  		},
  1943  	})
  1944  }
  1945  
  1946  func (s *storeActionSuite) TestSnapActionUnknownAction(c *C) {
  1947  	restore := release.MockOnClassic(false)
  1948  	defer restore()
  1949  
  1950  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1951  		c.Fatal("should not have made it to the server")
  1952  	}))
  1953  
  1954  	c.Assert(mockServer, NotNil)
  1955  	defer mockServer.Close()
  1956  
  1957  	mockServerURL, _ := url.Parse(mockServer.URL)
  1958  	cfg := store.Config{
  1959  		StoreBaseURL: mockServerURL,
  1960  	}
  1961  	dauthCtx := &testDauthContext{c: c, device: s.device}
  1962  	sto := store.New(&cfg, dauthCtx)
  1963  
  1964  	results, _, err := sto.SnapAction(s.ctx, nil,
  1965  		[]*store.SnapAction{
  1966  			{
  1967  				Action:       "something unexpected",
  1968  				InstanceName: "hello-world",
  1969  			},
  1970  		}, nil, nil, nil)
  1971  	c.Assert(err, ErrorMatches, `.* unsupported action .*`)
  1972  	c.Assert(results, IsNil)
  1973  }
  1974  
  1975  func (s *storeActionSuite) TestSnapActionErrorError(c *C) {
  1976  	e := &store.SnapActionError{Refresh: map[string]error{
  1977  		"foo": fmt.Errorf("sad refresh"),
  1978  	}}
  1979  	c.Check(e.Error(), Equals, `cannot refresh snap "foo": sad refresh`)
  1980  
  1981  	op, name, err := e.SingleOpError()
  1982  	c.Check(op, Equals, "refresh")
  1983  	c.Check(name, Equals, "foo")
  1984  	c.Check(err, ErrorMatches, "sad refresh")
  1985  
  1986  	e = &store.SnapActionError{Refresh: map[string]error{
  1987  		"foo": fmt.Errorf("sad refresh 1"),
  1988  		"bar": fmt.Errorf("sad refresh 2"),
  1989  	}}
  1990  	errMsg := e.Error()
  1991  	c.Check(strings.HasPrefix(errMsg, "cannot refresh:"), Equals, true)
  1992  	c.Check(errMsg, testutil.Contains, "\nsad refresh 1: \"foo\"")
  1993  	c.Check(errMsg, testutil.Contains, "\nsad refresh 2: \"bar\"")
  1994  
  1995  	op, name, err = e.SingleOpError()
  1996  	c.Check(op, Equals, "")
  1997  	c.Check(name, Equals, "")
  1998  	c.Check(err, IsNil)
  1999  
  2000  	e = &store.SnapActionError{Install: map[string]error{
  2001  		"foo": fmt.Errorf("sad install"),
  2002  	}}
  2003  	c.Check(e.Error(), Equals, `cannot install snap "foo": sad install`)
  2004  
  2005  	op, name, err = e.SingleOpError()
  2006  	c.Check(op, Equals, "install")
  2007  	c.Check(name, Equals, "foo")
  2008  	c.Check(err, ErrorMatches, "sad install")
  2009  
  2010  	e = &store.SnapActionError{Install: map[string]error{
  2011  		"foo": fmt.Errorf("sad install 1"),
  2012  		"bar": fmt.Errorf("sad install 2"),
  2013  	}}
  2014  	errMsg = e.Error()
  2015  	c.Check(strings.HasPrefix(errMsg, "cannot install:\n"), Equals, true)
  2016  	c.Check(errMsg, testutil.Contains, "\nsad install 1: \"foo\"")
  2017  	c.Check(errMsg, testutil.Contains, "\nsad install 2: \"bar\"")
  2018  
  2019  	op, name, err = e.SingleOpError()
  2020  	c.Check(op, Equals, "")
  2021  	c.Check(name, Equals, "")
  2022  	c.Check(err, IsNil)
  2023  
  2024  	e = &store.SnapActionError{Download: map[string]error{
  2025  		"foo": fmt.Errorf("sad download"),
  2026  	}}
  2027  	c.Check(e.Error(), Equals, `cannot download snap "foo": sad download`)
  2028  
  2029  	op, name, err = e.SingleOpError()
  2030  	c.Check(op, Equals, "download")
  2031  	c.Check(name, Equals, "foo")
  2032  	c.Check(err, ErrorMatches, "sad download")
  2033  
  2034  	e = &store.SnapActionError{Download: map[string]error{
  2035  		"foo": fmt.Errorf("sad download 1"),
  2036  		"bar": fmt.Errorf("sad download 2"),
  2037  	}}
  2038  	errMsg = e.Error()
  2039  	c.Check(strings.HasPrefix(errMsg, "cannot download:\n"), Equals, true)
  2040  	c.Check(errMsg, testutil.Contains, "\nsad download 1: \"foo\"")
  2041  	c.Check(errMsg, testutil.Contains, "\nsad download 2: \"bar\"")
  2042  
  2043  	op, name, err = e.SingleOpError()
  2044  	c.Check(op, Equals, "")
  2045  	c.Check(name, Equals, "")
  2046  	c.Check(err, IsNil)
  2047  
  2048  	e = &store.SnapActionError{Refresh: map[string]error{
  2049  		"foo": fmt.Errorf("sad refresh 1"),
  2050  	},
  2051  		Install: map[string]error{
  2052  			"bar": fmt.Errorf("sad install 2"),
  2053  		}}
  2054  	c.Check(e.Error(), Equals, `cannot refresh or install:
  2055  sad refresh 1: "foo"
  2056  sad install 2: "bar"`)
  2057  
  2058  	op, name, err = e.SingleOpError()
  2059  	c.Check(op, Equals, "")
  2060  	c.Check(name, Equals, "")
  2061  	c.Check(err, IsNil)
  2062  
  2063  	e = &store.SnapActionError{Refresh: map[string]error{
  2064  		"foo": fmt.Errorf("sad refresh 1"),
  2065  	},
  2066  		Download: map[string]error{
  2067  			"bar": fmt.Errorf("sad download 2"),
  2068  		}}
  2069  	c.Check(e.Error(), Equals, `cannot refresh or download:
  2070  sad refresh 1: "foo"
  2071  sad download 2: "bar"`)
  2072  
  2073  	op, name, err = e.SingleOpError()
  2074  	c.Check(op, Equals, "")
  2075  	c.Check(name, Equals, "")
  2076  	c.Check(err, IsNil)
  2077  
  2078  	e = &store.SnapActionError{Install: map[string]error{
  2079  		"foo": fmt.Errorf("sad install 1"),
  2080  	},
  2081  		Download: map[string]error{
  2082  			"bar": fmt.Errorf("sad download 2"),
  2083  		}}
  2084  	c.Check(e.Error(), Equals, `cannot install or download:
  2085  sad install 1: "foo"
  2086  sad download 2: "bar"`)
  2087  
  2088  	op, name, err = e.SingleOpError()
  2089  	c.Check(op, Equals, "")
  2090  	c.Check(name, Equals, "")
  2091  	c.Check(err, IsNil)
  2092  
  2093  	e = &store.SnapActionError{Refresh: map[string]error{
  2094  		"foo": fmt.Errorf("sad refresh 1"),
  2095  	},
  2096  		Install: map[string]error{
  2097  			"bar": fmt.Errorf("sad install 2"),
  2098  		},
  2099  		Download: map[string]error{
  2100  			"baz": fmt.Errorf("sad download 3"),
  2101  		}}
  2102  	c.Check(e.Error(), Equals, `cannot refresh, install, or download:
  2103  sad refresh 1: "foo"
  2104  sad install 2: "bar"
  2105  sad download 3: "baz"`)
  2106  
  2107  	op, name, err = e.SingleOpError()
  2108  	c.Check(op, Equals, "")
  2109  	c.Check(name, Equals, "")
  2110  	c.Check(err, IsNil)
  2111  
  2112  	e = &store.SnapActionError{
  2113  		NoResults: true,
  2114  		Other:     []error{fmt.Errorf("other error")},
  2115  	}
  2116  	c.Check(e.Error(), Equals, `cannot refresh, install, or download: other error`)
  2117  
  2118  	op, name, err = e.SingleOpError()
  2119  	c.Check(op, Equals, "")
  2120  	c.Check(name, Equals, "")
  2121  	c.Check(err, IsNil)
  2122  
  2123  	e = &store.SnapActionError{
  2124  		Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")},
  2125  	}
  2126  	c.Check(e.Error(), Equals, `cannot refresh, install, or download:
  2127  other error 1
  2128  other error 2`)
  2129  
  2130  	op, name, err = e.SingleOpError()
  2131  	c.Check(op, Equals, "")
  2132  	c.Check(name, Equals, "")
  2133  	c.Check(err, IsNil)
  2134  
  2135  	e = &store.SnapActionError{
  2136  		Install: map[string]error{
  2137  			"bar": fmt.Errorf("sad install"),
  2138  		},
  2139  		Other: []error{fmt.Errorf("other error 1"), fmt.Errorf("other error 2")},
  2140  	}
  2141  	c.Check(e.Error(), Equals, `cannot refresh, install, or download:
  2142  sad install: "bar"
  2143  other error 1
  2144  other error 2`)
  2145  
  2146  	op, name, err = e.SingleOpError()
  2147  	c.Check(op, Equals, "")
  2148  	c.Check(name, Equals, "")
  2149  	c.Check(err, IsNil)
  2150  
  2151  	e = &store.SnapActionError{
  2152  		NoResults: true,
  2153  	}
  2154  	c.Check(e.Error(), Equals, "no install/refresh information results from the store")
  2155  
  2156  	op, name, err = e.SingleOpError()
  2157  	c.Check(op, Equals, "")
  2158  	c.Check(name, Equals, "")
  2159  	c.Check(err, IsNil)
  2160  }
  2161  
  2162  func (s *storeActionSuite) TestSnapActionRefreshesBothAuths(c *C) {
  2163  	// snap action (install/refresh) has is its own custom way to
  2164  	// signal macaroon refreshes that allows to do a best effort
  2165  	// with the available results
  2166  
  2167  	refresh, err := makeTestRefreshDischargeResponse()
  2168  	c.Assert(err, IsNil)
  2169  	c.Check(s.user.StoreDischarges[0], Not(Equals), refresh)
  2170  
  2171  	// mock refresh response
  2172  	refreshDischargeEndpointHit := false
  2173  	mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2174  		io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
  2175  		refreshDischargeEndpointHit = true
  2176  	}))
  2177  	defer mockSSOServer.Close()
  2178  	store.UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
  2179  
  2180  	refreshSessionRequested := false
  2181  	expiredAuth := `Macaroon root="expired-session-macaroon"`
  2182  	n := 0
  2183  	// mock store response
  2184  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2185  		c.Check(r.UserAgent(), Equals, userAgent)
  2186  
  2187  		switch r.URL.Path {
  2188  		case snapActionPath:
  2189  			n++
  2190  			type errObj struct {
  2191  				Code    string `json:"code"`
  2192  				Message string `json:"message"`
  2193  			}
  2194  			var errors []errObj
  2195  
  2196  			authorization := r.Header.Get("Authorization")
  2197  			c.Check(authorization, Equals, expectedAuthorization(c, s.user))
  2198  			if s.user.StoreDischarges[0] != refresh {
  2199  				errors = append(errors, errObj{Code: "user-authorization-needs-refresh"})
  2200  			}
  2201  
  2202  			devAuthorization := r.Header.Get("Snap-Device-Authorization")
  2203  			if devAuthorization == "" {
  2204  				c.Fatalf("device authentication missing")
  2205  			} else if devAuthorization == expiredAuth {
  2206  				errors = append(errors, errObj{Code: "device-authorization-needs-refresh"})
  2207  			} else {
  2208  				c.Check(devAuthorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
  2209  			}
  2210  
  2211  			errorsJSON, err := json.Marshal(errors)
  2212  			c.Assert(err, IsNil)
  2213  
  2214  			io.WriteString(w, fmt.Sprintf(`{
  2215    "results": [{
  2216       "result": "refresh",
  2217       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2218       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2219       "name": "hello-world",
  2220       "snap": {
  2221         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2222         "name": "hello-world",
  2223         "revision": 26,
  2224         "version": "6.1",
  2225         "publisher": {
  2226            "id": "canonical",
  2227            "name": "canonical",
  2228            "title": "Canonical"
  2229         }
  2230       }
  2231    }],
  2232    "error-list": %s
  2233  }`, errorsJSON))
  2234  		case authNoncesPath:
  2235  			io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
  2236  		case authSessionPath:
  2237  			// sanity of request
  2238  			jsonReq, err := ioutil.ReadAll(r.Body)
  2239  			c.Assert(err, IsNil)
  2240  			var req map[string]string
  2241  			err = json.Unmarshal(jsonReq, &req)
  2242  			c.Assert(err, IsNil)
  2243  			c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true)
  2244  			c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true)
  2245  			c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true)
  2246  
  2247  			authorization := r.Header.Get("X-Device-Authorization")
  2248  			if authorization == "" {
  2249  				c.Fatalf("expecting only refresh")
  2250  			} else {
  2251  				c.Check(authorization, Equals, expiredAuth)
  2252  				io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
  2253  				refreshSessionRequested = true
  2254  			}
  2255  		default:
  2256  			c.Fatalf("unexpected path %q", r.URL.Path)
  2257  		}
  2258  	}))
  2259  	c.Assert(mockServer, NotNil)
  2260  	defer mockServer.Close()
  2261  
  2262  	mockServerURL, _ := url.Parse(mockServer.URL)
  2263  
  2264  	// make sure device session is expired
  2265  	s.device.SessionMacaroon = "expired-session-macaroon"
  2266  	dauthCtx := &testDauthContext{c: c, device: s.device, user: s.user}
  2267  	sto := store.New(&store.Config{
  2268  		StoreBaseURL: mockServerURL,
  2269  	}, dauthCtx)
  2270  
  2271  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2272  		{
  2273  			InstanceName:    "hello-world",
  2274  			SnapID:          helloWorldSnapID,
  2275  			TrackingChannel: "beta",
  2276  			Revision:        snap.R(1),
  2277  			RefreshedDate:   helloRefreshedDate,
  2278  		},
  2279  	}, []*store.SnapAction{
  2280  		{
  2281  			Action:       "refresh",
  2282  			SnapID:       helloWorldSnapID,
  2283  			InstanceName: "hello-world",
  2284  		},
  2285  	}, nil, s.user, nil)
  2286  	c.Assert(err, IsNil)
  2287  	c.Assert(results, HasLen, 1)
  2288  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  2289  	c.Check(refreshDischargeEndpointHit, Equals, true)
  2290  	c.Check(refreshSessionRequested, Equals, true)
  2291  	c.Check(n, Equals, 2)
  2292  }
  2293  
  2294  func (s *storeActionSuite) TestSnapActionRefreshParallelInstall(c *C) {
  2295  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2296  		assertRequest(c, r, "POST", snapActionPath)
  2297  		// check device authorization is set, implicitly checking doRequest was used
  2298  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2299  
  2300  		jsonReq, err := ioutil.ReadAll(r.Body)
  2301  		c.Assert(err, IsNil)
  2302  		var req struct {
  2303  			Context []map[string]interface{} `json:"context"`
  2304  			Actions []map[string]interface{} `json:"actions"`
  2305  		}
  2306  
  2307  		err = json.Unmarshal(jsonReq, &req)
  2308  		c.Assert(err, IsNil)
  2309  
  2310  		c.Assert(req.Context, HasLen, 2)
  2311  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2312  			"snap-id":          helloWorldSnapID,
  2313  			"instance-key":     helloWorldSnapID,
  2314  			"revision":         float64(26),
  2315  			"tracking-channel": "stable",
  2316  			"refreshed-date":   helloRefreshedDateStr,
  2317  			"epoch":            iZeroEpoch,
  2318  		})
  2319  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2320  			"snap-id":          helloWorldSnapID,
  2321  			"instance-key":     helloWorldFooInstanceKeyWithSalt,
  2322  			"revision":         float64(2),
  2323  			"tracking-channel": "stable",
  2324  			"refreshed-date":   helloRefreshedDateStr,
  2325  			"epoch":            iZeroEpoch,
  2326  		})
  2327  		c.Assert(req.Actions, HasLen, 1)
  2328  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2329  			"action":       "refresh",
  2330  			"instance-key": helloWorldFooInstanceKeyWithSalt,
  2331  			"snap-id":      helloWorldSnapID,
  2332  			"channel":      "stable",
  2333  		})
  2334  
  2335  		io.WriteString(w, `{
  2336    "results": [{
  2337       "result": "refresh",
  2338       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk",
  2339       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2340       "name": "hello-world",
  2341       "snap": {
  2342         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2343         "name": "hello-world",
  2344         "revision": 26,
  2345         "version": "6.1",
  2346         "publisher": {
  2347            "id": "canonical",
  2348            "username": "canonical",
  2349            "display-name": "Canonical"
  2350         }
  2351       }
  2352    }]
  2353  }`)
  2354  	}))
  2355  
  2356  	c.Assert(mockServer, NotNil)
  2357  	defer mockServer.Close()
  2358  
  2359  	mockServerURL, _ := url.Parse(mockServer.URL)
  2360  	cfg := store.Config{
  2361  		StoreBaseURL: mockServerURL,
  2362  	}
  2363  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2364  	sto := store.New(&cfg, dauthCtx)
  2365  
  2366  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2367  		{
  2368  			InstanceName:    "hello-world",
  2369  			SnapID:          helloWorldSnapID,
  2370  			TrackingChannel: "stable",
  2371  			Revision:        snap.R(26),
  2372  			RefreshedDate:   helloRefreshedDate,
  2373  		}, {
  2374  			InstanceName:    "hello-world_foo",
  2375  			SnapID:          helloWorldSnapID,
  2376  			TrackingChannel: "stable",
  2377  			Revision:        snap.R(2),
  2378  			RefreshedDate:   helloRefreshedDate,
  2379  		},
  2380  	}, []*store.SnapAction{
  2381  		{
  2382  			Action:       "refresh",
  2383  			SnapID:       helloWorldSnapID,
  2384  			Channel:      "stable",
  2385  			InstanceName: "hello-world_foo",
  2386  		},
  2387  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  2388  	c.Assert(err, IsNil)
  2389  	c.Assert(results, HasLen, 1)
  2390  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2391  	c.Assert(results[0].InstanceName(), Equals, "hello-world_foo")
  2392  	c.Assert(results[0].Revision, Equals, snap.R(26))
  2393  }
  2394  
  2395  func (s *storeActionSuite) TestSnapActionRefreshStableInstanceKey(c *C) {
  2396  	// salt "foo"
  2397  	helloWorldFooInstanceKeyWithSaltFoo := helloWorldSnapID + ":CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI"
  2398  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2399  		assertRequest(c, r, "POST", snapActionPath)
  2400  		// check device authorization is set, implicitly checking doRequest was used
  2401  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2402  
  2403  		jsonReq, err := ioutil.ReadAll(r.Body)
  2404  		c.Assert(err, IsNil)
  2405  		var req struct {
  2406  			Context []map[string]interface{} `json:"context"`
  2407  			Actions []map[string]interface{} `json:"actions"`
  2408  		}
  2409  
  2410  		err = json.Unmarshal(jsonReq, &req)
  2411  		c.Assert(err, IsNil)
  2412  
  2413  		c.Assert(req.Context, HasLen, 2)
  2414  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2415  			"snap-id":          helloWorldSnapID,
  2416  			"instance-key":     helloWorldSnapID,
  2417  			"revision":         float64(26),
  2418  			"tracking-channel": "stable",
  2419  			"refreshed-date":   helloRefreshedDateStr,
  2420  			"epoch":            iZeroEpoch,
  2421  			"cohort-key":       "what",
  2422  		})
  2423  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2424  			"snap-id":          helloWorldSnapID,
  2425  			"instance-key":     helloWorldFooInstanceKeyWithSaltFoo,
  2426  			"revision":         float64(2),
  2427  			"tracking-channel": "stable",
  2428  			"refreshed-date":   helloRefreshedDateStr,
  2429  			"epoch":            iZeroEpoch,
  2430  		})
  2431  		c.Assert(req.Actions, HasLen, 1)
  2432  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2433  			"action":       "refresh",
  2434  			"instance-key": helloWorldFooInstanceKeyWithSaltFoo,
  2435  			"snap-id":      helloWorldSnapID,
  2436  			"channel":      "stable",
  2437  		})
  2438  
  2439  		io.WriteString(w, `{
  2440    "results": [{
  2441       "result": "refresh",
  2442       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:CY2pHZ7nlQDuiO5DxIsdRttcqqBoD2ZCQiEtCJSdVcI",
  2443       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2444       "name": "hello-world",
  2445       "snap": {
  2446         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2447         "name": "hello-world",
  2448         "revision": 26,
  2449         "version": "6.1",
  2450         "publisher": {
  2451            "id": "canonical",
  2452            "username": "canonical",
  2453            "display-name": "Canonical"
  2454         }
  2455       }
  2456    }]
  2457  }`)
  2458  	}))
  2459  
  2460  	c.Assert(mockServer, NotNil)
  2461  	defer mockServer.Close()
  2462  
  2463  	mockServerURL, _ := url.Parse(mockServer.URL)
  2464  	cfg := store.Config{
  2465  		StoreBaseURL: mockServerURL,
  2466  	}
  2467  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2468  	sto := store.New(&cfg, dauthCtx)
  2469  
  2470  	opts := &store.RefreshOptions{PrivacyKey: "foo"}
  2471  	currentSnaps := []*store.CurrentSnap{
  2472  		{
  2473  			InstanceName:    "hello-world",
  2474  			SnapID:          helloWorldSnapID,
  2475  			TrackingChannel: "stable",
  2476  			Revision:        snap.R(26),
  2477  			RefreshedDate:   helloRefreshedDate,
  2478  			CohortKey:       "what",
  2479  		}, {
  2480  			InstanceName:    "hello-world_foo",
  2481  			SnapID:          helloWorldSnapID,
  2482  			TrackingChannel: "stable",
  2483  			Revision:        snap.R(2),
  2484  			RefreshedDate:   helloRefreshedDate,
  2485  		},
  2486  	}
  2487  	action := []*store.SnapAction{
  2488  		{
  2489  			Action:       "refresh",
  2490  			SnapID:       helloWorldSnapID,
  2491  			Channel:      "stable",
  2492  			InstanceName: "hello-world_foo",
  2493  		},
  2494  	}
  2495  	results, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts)
  2496  	c.Assert(err, IsNil)
  2497  	c.Assert(results, HasLen, 1)
  2498  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2499  	c.Assert(results[0].InstanceName(), Equals, "hello-world_foo")
  2500  	c.Assert(results[0].Revision, Equals, snap.R(26))
  2501  
  2502  	// another request with the same seed, gives same result
  2503  	resultsAgain, _, err := sto.SnapAction(s.ctx, currentSnaps, action, nil, nil, opts)
  2504  	c.Assert(err, IsNil)
  2505  	c.Assert(resultsAgain, DeepEquals, results)
  2506  }
  2507  
  2508  func (s *storeActionSuite) TestSnapActionRefreshWithValidationSets(c *C) {
  2509  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2510  		assertRequest(c, r, "POST", snapActionPath)
  2511  		// check device authorization is set, implicitly checking doRequest was used
  2512  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2513  
  2514  		jsonReq, err := ioutil.ReadAll(r.Body)
  2515  		c.Assert(err, IsNil)
  2516  		var req struct {
  2517  			Context []map[string]interface{} `json:"context"`
  2518  			Actions []map[string]interface{} `json:"actions"`
  2519  		}
  2520  
  2521  		err = json.Unmarshal(jsonReq, &req)
  2522  		c.Assert(err, IsNil)
  2523  
  2524  		c.Assert(req.Context, HasLen, 1)
  2525  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2526  			"snap-id":          helloWorldSnapID,
  2527  			"instance-key":     helloWorldSnapID,
  2528  			"revision":         float64(1),
  2529  			"tracking-channel": "stable",
  2530  			"refreshed-date":   helloRefreshedDateStr,
  2531  			"epoch":            iZeroEpoch,
  2532  			"validation-sets":  []interface{}{[]interface{}{"foo", "bar"}, []interface{}{"foo", "baz"}},
  2533  		})
  2534  		c.Assert(req.Actions, HasLen, 1)
  2535  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2536  			"action":       "refresh",
  2537  			"instance-key": helloWorldSnapID,
  2538  			"snap-id":      helloWorldSnapID,
  2539  			"channel":      "stable",
  2540  		})
  2541  
  2542  		io.WriteString(w, `{
  2543    "results": [{
  2544       "result": "refresh",
  2545       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2546       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2547       "name": "hello-world",
  2548       "snap": {
  2549         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2550         "name": "hello-world",
  2551         "revision": 26,
  2552         "version": "6.1",
  2553         "publisher": {
  2554            "id": "canonical",
  2555            "username": "canonical",
  2556            "display-name": "Canonical"
  2557         }
  2558       }
  2559    }]
  2560  }`)
  2561  	}))
  2562  
  2563  	c.Assert(mockServer, NotNil)
  2564  	defer mockServer.Close()
  2565  
  2566  	mockServerURL, _ := url.Parse(mockServer.URL)
  2567  	cfg := store.Config{
  2568  		StoreBaseURL: mockServerURL,
  2569  	}
  2570  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2571  	sto := store.New(&cfg, dauthCtx)
  2572  
  2573  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2574  		{
  2575  			InstanceName:    "hello-world",
  2576  			SnapID:          helloWorldSnapID,
  2577  			TrackingChannel: "stable",
  2578  			Revision:        snap.R(1),
  2579  			RefreshedDate:   helloRefreshedDate,
  2580  			ValidationSets:  [][]string{{"foo", "bar"}, {"foo", "baz"}},
  2581  		},
  2582  	}, []*store.SnapAction{
  2583  		{
  2584  			Action:       "refresh",
  2585  			SnapID:       helloWorldSnapID,
  2586  			Channel:      "stable",
  2587  			InstanceName: "hello-world",
  2588  		},
  2589  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  2590  	c.Assert(err, IsNil)
  2591  	c.Assert(results, HasLen, 1)
  2592  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2593  	c.Assert(results[0].InstanceName(), Equals, "hello-world")
  2594  	c.Assert(results[0].Revision, Equals, snap.R(26))
  2595  }
  2596  
  2597  func (s *storeActionSuite) TestSnapActionRevisionNotAvailableParallelInstall(c *C) {
  2598  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2599  		assertRequest(c, r, "POST", snapActionPath)
  2600  		// check device authorization is set, implicitly checking doRequest was used
  2601  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2602  
  2603  		jsonReq, err := ioutil.ReadAll(r.Body)
  2604  		c.Assert(err, IsNil)
  2605  		var req struct {
  2606  			Context []map[string]interface{} `json:"context"`
  2607  			Actions []map[string]interface{} `json:"actions"`
  2608  		}
  2609  
  2610  		err = json.Unmarshal(jsonReq, &req)
  2611  		c.Assert(err, IsNil)
  2612  
  2613  		c.Assert(req.Context, HasLen, 2)
  2614  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2615  			"snap-id":          helloWorldSnapID,
  2616  			"instance-key":     helloWorldSnapID,
  2617  			"revision":         float64(26),
  2618  			"tracking-channel": "stable",
  2619  			"refreshed-date":   helloRefreshedDateStr,
  2620  			"epoch":            iZeroEpoch,
  2621  		})
  2622  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  2623  			"snap-id":          helloWorldSnapID,
  2624  			"instance-key":     helloWorldFooInstanceKeyWithSalt,
  2625  			"revision":         float64(2),
  2626  			"tracking-channel": "edge",
  2627  			"refreshed-date":   helloRefreshedDateStr,
  2628  			"epoch":            iZeroEpoch,
  2629  		})
  2630  		c.Assert(req.Actions, HasLen, 3)
  2631  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2632  			"action":       "refresh",
  2633  			"instance-key": helloWorldSnapID,
  2634  			"snap-id":      helloWorldSnapID,
  2635  		})
  2636  		c.Assert(req.Actions[1], DeepEquals, map[string]interface{}{
  2637  			"action":       "refresh",
  2638  			"instance-key": helloWorldFooInstanceKeyWithSalt,
  2639  			"snap-id":      helloWorldSnapID,
  2640  		})
  2641  		c.Assert(req.Actions[2], DeepEquals, map[string]interface{}{
  2642  			"action":       "install",
  2643  			"instance-key": "install-1",
  2644  			"name":         "other",
  2645  			"channel":      "stable",
  2646  			"epoch":        nil,
  2647  		})
  2648  
  2649  		io.WriteString(w, `{
  2650    "results": [{
  2651       "result": "error",
  2652       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2653       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2654       "name": "hello-world",
  2655       "error": {
  2656         "code": "revision-not-found",
  2657         "message": "msg1"
  2658       }
  2659    }, {
  2660       "result": "error",
  2661       "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk",
  2662       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2663       "name": "hello-world",
  2664       "error": {
  2665         "code": "revision-not-found",
  2666         "message": "msg2"
  2667       }
  2668    },  {
  2669       "result": "error",
  2670       "instance-key": "install-1",
  2671       "snap-id": "foo-id",
  2672       "name": "other",
  2673       "error": {
  2674         "code": "revision-not-found",
  2675         "message": "msg3"
  2676       }
  2677    }
  2678    ]
  2679  }`)
  2680  	}))
  2681  
  2682  	c.Assert(mockServer, NotNil)
  2683  	defer mockServer.Close()
  2684  
  2685  	mockServerURL, _ := url.Parse(mockServer.URL)
  2686  	cfg := store.Config{
  2687  		StoreBaseURL: mockServerURL,
  2688  	}
  2689  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2690  	sto := store.New(&cfg, dauthCtx)
  2691  
  2692  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2693  		{
  2694  			InstanceName:    "hello-world",
  2695  			SnapID:          helloWorldSnapID,
  2696  			TrackingChannel: "stable",
  2697  			Revision:        snap.R(26),
  2698  			RefreshedDate:   helloRefreshedDate,
  2699  		},
  2700  		{
  2701  			InstanceName:    "hello-world_foo",
  2702  			SnapID:          helloWorldSnapID,
  2703  			TrackingChannel: "edge",
  2704  			Revision:        snap.R(2),
  2705  			RefreshedDate:   helloRefreshedDate,
  2706  		},
  2707  	}, []*store.SnapAction{
  2708  		{
  2709  			Action:       "refresh",
  2710  			InstanceName: "hello-world",
  2711  			SnapID:       helloWorldSnapID,
  2712  		}, {
  2713  			Action:       "refresh",
  2714  			InstanceName: "hello-world_foo",
  2715  			SnapID:       helloWorldSnapID,
  2716  		}, {
  2717  			Action:       "install",
  2718  			InstanceName: "other_foo",
  2719  			Channel:      "stable",
  2720  		},
  2721  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  2722  	c.Assert(results, HasLen, 0)
  2723  	c.Check(err, DeepEquals, &store.SnapActionError{
  2724  		Refresh: map[string]error{
  2725  			"hello-world": &store.RevisionNotAvailableError{
  2726  				Action:  "refresh",
  2727  				Channel: "stable",
  2728  			},
  2729  			"hello-world_foo": &store.RevisionNotAvailableError{
  2730  				Action:  "refresh",
  2731  				Channel: "edge",
  2732  			},
  2733  		},
  2734  		Install: map[string]error{
  2735  			"other_foo": &store.RevisionNotAvailableError{
  2736  				Action:  "install",
  2737  				Channel: "stable",
  2738  			},
  2739  		},
  2740  	})
  2741  }
  2742  
  2743  func (s *storeActionSuite) TestSnapActionInstallParallelInstall(c *C) {
  2744  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2745  		assertRequest(c, r, "POST", snapActionPath)
  2746  		// check device authorization is set, implicitly checking doRequest was used
  2747  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2748  
  2749  		jsonReq, err := ioutil.ReadAll(r.Body)
  2750  		c.Assert(err, IsNil)
  2751  		var req struct {
  2752  			Context []map[string]interface{} `json:"context"`
  2753  			Actions []map[string]interface{} `json:"actions"`
  2754  		}
  2755  
  2756  		err = json.Unmarshal(jsonReq, &req)
  2757  		c.Assert(err, IsNil)
  2758  
  2759  		c.Assert(req.Context, HasLen, 1)
  2760  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2761  			"snap-id":          helloWorldSnapID,
  2762  			"instance-key":     helloWorldSnapID,
  2763  			"revision":         float64(26),
  2764  			"tracking-channel": "stable",
  2765  			"refreshed-date":   helloRefreshedDateStr,
  2766  			"epoch":            iZeroEpoch,
  2767  		})
  2768  		c.Assert(req.Actions, HasLen, 1)
  2769  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2770  			"action":       "install",
  2771  			"instance-key": "install-1",
  2772  			"name":         "hello-world",
  2773  			"channel":      "stable",
  2774  			"epoch":        nil,
  2775  		})
  2776  
  2777  		io.WriteString(w, `{
  2778    "results": [{
  2779       "result": "install",
  2780       "instance-key": "install-1",
  2781       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2782       "name": "hello-world",
  2783       "snap": {
  2784         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2785         "name": "hello-world",
  2786         "revision": 28,
  2787         "version": "6.1",
  2788         "publisher": {
  2789            "id": "canonical",
  2790            "username": "canonical",
  2791            "display-name": "Canonical"
  2792         }
  2793       }
  2794    }]
  2795  }`)
  2796  	}))
  2797  
  2798  	c.Assert(mockServer, NotNil)
  2799  	defer mockServer.Close()
  2800  
  2801  	mockServerURL, _ := url.Parse(mockServer.URL)
  2802  	cfg := store.Config{
  2803  		StoreBaseURL: mockServerURL,
  2804  	}
  2805  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2806  	sto := store.New(&cfg, dauthCtx)
  2807  
  2808  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2809  		{
  2810  			InstanceName:    "hello-world",
  2811  			SnapID:          helloWorldSnapID,
  2812  			TrackingChannel: "stable",
  2813  			Revision:        snap.R(26),
  2814  			RefreshedDate:   helloRefreshedDate,
  2815  		},
  2816  	}, []*store.SnapAction{
  2817  		{
  2818  			Action:       "install",
  2819  			InstanceName: "hello-world_foo",
  2820  			Channel:      "stable",
  2821  		},
  2822  	}, nil, nil, nil)
  2823  	c.Assert(err, IsNil)
  2824  	c.Assert(results, HasLen, 1)
  2825  	c.Assert(results[0].InstanceName(), Equals, "hello-world_foo")
  2826  	c.Assert(results[0].SnapName(), Equals, "hello-world")
  2827  	c.Assert(results[0].Revision, Equals, snap.R(28))
  2828  	c.Assert(results[0].Version, Equals, "6.1")
  2829  	c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
  2830  	c.Assert(results[0].Deltas, HasLen, 0)
  2831  	// effective-channel is not set
  2832  	c.Assert(results[0].Channel, Equals, "")
  2833  }
  2834  
  2835  func (s *storeActionSuite) TestSnapActionErrorsWhenNoInstanceName(c *C) {
  2836  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2837  	sto := store.New(&store.Config{}, dauthCtx)
  2838  
  2839  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2840  		{
  2841  			InstanceName:    "hello-world",
  2842  			SnapID:          helloWorldSnapID,
  2843  			TrackingChannel: "stable",
  2844  			Revision:        snap.R(26),
  2845  			RefreshedDate:   helloRefreshedDate,
  2846  		},
  2847  	}, []*store.SnapAction{
  2848  		{
  2849  			Action:  "install",
  2850  			Channel: "stable",
  2851  		},
  2852  	}, nil, nil, nil)
  2853  	c.Assert(err, ErrorMatches, "internal error: action without instance name")
  2854  	c.Assert(results, IsNil)
  2855  }
  2856  
  2857  func (s *storeActionSuite) TestSnapActionInstallUnexpectedInstallKey(c *C) {
  2858  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2859  		assertRequest(c, r, "POST", snapActionPath)
  2860  		// check device authorization is set, implicitly checking doRequest was used
  2861  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2862  
  2863  		jsonReq, err := ioutil.ReadAll(r.Body)
  2864  		c.Assert(err, IsNil)
  2865  		var req struct {
  2866  			Context []map[string]interface{} `json:"context"`
  2867  			Actions []map[string]interface{} `json:"actions"`
  2868  		}
  2869  
  2870  		err = json.Unmarshal(jsonReq, &req)
  2871  		c.Assert(err, IsNil)
  2872  
  2873  		c.Assert(req.Context, HasLen, 1)
  2874  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2875  			"snap-id":          helloWorldSnapID,
  2876  			"instance-key":     helloWorldSnapID,
  2877  			"revision":         float64(26),
  2878  			"tracking-channel": "stable",
  2879  			"refreshed-date":   helloRefreshedDateStr,
  2880  			"epoch":            iZeroEpoch,
  2881  		})
  2882  		c.Assert(req.Actions, HasLen, 1)
  2883  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2884  			"action":       "install",
  2885  			"instance-key": "install-1",
  2886  			"name":         "hello-world",
  2887  			"channel":      "stable",
  2888  			"epoch":        nil,
  2889  		})
  2890  
  2891  		io.WriteString(w, `{
  2892    "results": [{
  2893       "result": "install",
  2894       "instance-key": "foo-2",
  2895       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2896       "name": "hello-world",
  2897       "snap": {
  2898         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2899         "name": "hello-world",
  2900         "revision": 28,
  2901         "version": "6.1",
  2902         "publisher": {
  2903            "id": "canonical",
  2904            "username": "canonical",
  2905            "display-name": "Canonical"
  2906         }
  2907       }
  2908    }]
  2909  }`)
  2910  	}))
  2911  
  2912  	c.Assert(mockServer, NotNil)
  2913  	defer mockServer.Close()
  2914  
  2915  	mockServerURL, _ := url.Parse(mockServer.URL)
  2916  	cfg := store.Config{
  2917  		StoreBaseURL: mockServerURL,
  2918  	}
  2919  	dauthCtx := &testDauthContext{c: c, device: s.device}
  2920  	sto := store.New(&cfg, dauthCtx)
  2921  
  2922  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  2923  		{
  2924  			InstanceName:    "hello-world",
  2925  			SnapID:          helloWorldSnapID,
  2926  			TrackingChannel: "stable",
  2927  			Revision:        snap.R(26),
  2928  			RefreshedDate:   helloRefreshedDate,
  2929  		},
  2930  	}, []*store.SnapAction{
  2931  		{
  2932  			Action:       "install",
  2933  			InstanceName: "hello-world_foo",
  2934  			Channel:      "stable",
  2935  		},
  2936  	}, nil, nil, nil)
  2937  	c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected instance-key "foo-2"`)
  2938  	c.Assert(results, IsNil)
  2939  }
  2940  
  2941  func (s *storeActionSuite) TestSnapActionRefreshUnexpectedInstanceKey(c *C) {
  2942  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  2943  		assertRequest(c, r, "POST", snapActionPath)
  2944  		// check device authorization is set, implicitly checking doRequest was used
  2945  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  2946  
  2947  		jsonReq, err := ioutil.ReadAll(r.Body)
  2948  		c.Assert(err, IsNil)
  2949  		var req struct {
  2950  			Context []map[string]interface{} `json:"context"`
  2951  			Actions []map[string]interface{} `json:"actions"`
  2952  		}
  2953  
  2954  		err = json.Unmarshal(jsonReq, &req)
  2955  		c.Assert(err, IsNil)
  2956  
  2957  		c.Assert(req.Context, HasLen, 1)
  2958  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  2959  			"snap-id":          helloWorldSnapID,
  2960  			"instance-key":     helloWorldSnapID,
  2961  			"revision":         float64(26),
  2962  			"tracking-channel": "stable",
  2963  			"refreshed-date":   helloRefreshedDateStr,
  2964  			"epoch":            iZeroEpoch,
  2965  		})
  2966  		c.Assert(req.Actions, HasLen, 1)
  2967  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  2968  			"action":       "refresh",
  2969  			"instance-key": helloWorldSnapID,
  2970  			"snap-id":      helloWorldSnapID,
  2971  			"channel":      "stable",
  2972  		})
  2973  
  2974  		io.WriteString(w, `{
  2975    "results": [{
  2976       "result": "refresh",
  2977       "instance-key": "foo-5",
  2978       "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2979       "name": "hello-world",
  2980       "snap": {
  2981         "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
  2982         "name": "hello-world",
  2983         "revision": 26,
  2984         "version": "6.1",
  2985         "publisher": {
  2986            "id": "canonical",
  2987            "username": "canonical",
  2988            "display-name": "Canonical"
  2989         }
  2990       }
  2991    }]
  2992  }`)
  2993  	}))
  2994  
  2995  	c.Assert(mockServer, NotNil)
  2996  	defer mockServer.Close()
  2997  
  2998  	mockServerURL, _ := url.Parse(mockServer.URL)
  2999  	cfg := store.Config{
  3000  		StoreBaseURL: mockServerURL,
  3001  	}
  3002  	dauthCtx := &testDauthContext{c: c, device: s.device}
  3003  	sto := store.New(&cfg, dauthCtx)
  3004  
  3005  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  3006  		{
  3007  			InstanceName:    "hello-world",
  3008  			SnapID:          helloWorldSnapID,
  3009  			TrackingChannel: "stable",
  3010  			Revision:        snap.R(26),
  3011  			RefreshedDate:   helloRefreshedDate,
  3012  		},
  3013  	}, []*store.SnapAction{
  3014  		{
  3015  			Action:       "refresh",
  3016  			SnapID:       helloWorldSnapID,
  3017  			Channel:      "stable",
  3018  			InstanceName: "hello-world",
  3019  		},
  3020  	}, nil, nil, nil)
  3021  	c.Assert(err, ErrorMatches, `unexpected invalid install/refresh API result: unexpected refresh`)
  3022  	c.Assert(results, IsNil)
  3023  }
  3024  
  3025  func (s *storeActionSuite) TestSnapActionUnexpectedErrorKey(c *C) {
  3026  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3027  		assertRequest(c, r, "POST", snapActionPath)
  3028  		// check device authorization is set, implicitly checking doRequest was used
  3029  		c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
  3030  
  3031  		jsonReq, err := ioutil.ReadAll(r.Body)
  3032  		c.Assert(err, IsNil)
  3033  		var req struct {
  3034  			Context []map[string]interface{} `json:"context"`
  3035  			Actions []map[string]interface{} `json:"actions"`
  3036  		}
  3037  
  3038  		err = json.Unmarshal(jsonReq, &req)
  3039  		c.Assert(err, IsNil)
  3040  
  3041  		c.Assert(req.Context, HasLen, 2)
  3042  		c.Assert(req.Context[0], DeepEquals, map[string]interface{}{
  3043  			"snap-id":          helloWorldSnapID,
  3044  			"instance-key":     helloWorldSnapID,
  3045  			"revision":         float64(26),
  3046  			"tracking-channel": "stable",
  3047  			"refreshed-date":   helloRefreshedDateStr,
  3048  			"epoch":            iZeroEpoch,
  3049  		})
  3050  		c.Assert(req.Context[1], DeepEquals, map[string]interface{}{
  3051  			"snap-id":          helloWorldSnapID,
  3052  			"instance-key":     helloWorldFooInstanceKeyWithSalt,
  3053  			"revision":         float64(2),
  3054  			"tracking-channel": "stable",
  3055  			"refreshed-date":   helloRefreshedDateStr,
  3056  			"epoch":            iZeroEpoch,
  3057  		})
  3058  		c.Assert(req.Actions, HasLen, 1)
  3059  		c.Assert(req.Actions[0], DeepEquals, map[string]interface{}{
  3060  			"action":       "install",
  3061  			"instance-key": "install-1",
  3062  			"name":         "foo-2",
  3063  			"epoch":        nil,
  3064  		})
  3065  
  3066  		io.WriteString(w, `{
  3067    "results": [{
  3068       "result": "install",
  3069       "instance-key": "install-1",
  3070       "snap-id": "foo-2-id",
  3071       "name": "foo-2",
  3072       "snap": {
  3073         "snap-id": "foo-2-id",
  3074         "name": "foo-2",
  3075         "revision": 28,
  3076         "version": "6.1",
  3077         "publisher": {
  3078            "id": "canonical",
  3079            "username": "canonical",
  3080            "display-name": "Canonical"
  3081         }
  3082       }
  3083    },{
  3084        "error": {
  3085          "code": "duplicated-snap",
  3086           "message": "The Snap is present more than once in the request."
  3087        },
  3088        "instance-key": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ:IDKVhLy-HUyfYGFKcsH4V-7FVG7hLGs4M5zsraZU5tk",
  3089        "name": null,
  3090        "result": "error",
  3091        "snap": null,
  3092        "snap-id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
  3093    }]
  3094  }`)
  3095  	}))
  3096  
  3097  	c.Assert(mockServer, NotNil)
  3098  	defer mockServer.Close()
  3099  
  3100  	mockServerURL, _ := url.Parse(mockServer.URL)
  3101  	cfg := store.Config{
  3102  		StoreBaseURL: mockServerURL,
  3103  	}
  3104  	dauthCtx := &testDauthContext{c: c, device: s.device}
  3105  	sto := store.New(&cfg, dauthCtx)
  3106  
  3107  	results, _, err := sto.SnapAction(s.ctx, []*store.CurrentSnap{
  3108  		{
  3109  			InstanceName:    "hello-world",
  3110  			SnapID:          helloWorldSnapID,
  3111  			TrackingChannel: "stable",
  3112  			Revision:        snap.R(26),
  3113  			RefreshedDate:   helloRefreshedDate,
  3114  		}, {
  3115  			InstanceName:    "hello-world_foo",
  3116  			SnapID:          helloWorldSnapID,
  3117  			TrackingChannel: "stable",
  3118  			Revision:        snap.R(2),
  3119  			RefreshedDate:   helloRefreshedDate,
  3120  		},
  3121  	}, []*store.SnapAction{
  3122  		{
  3123  			Action:       "install",
  3124  			InstanceName: "foo-2",
  3125  		},
  3126  	}, nil, nil, &store.RefreshOptions{PrivacyKey: "123"})
  3127  	c.Assert(err, DeepEquals, &store.SnapActionError{
  3128  		Other: []error{fmt.Errorf(`snap "hello-world_foo": The Snap is present more than once in the request.`)},
  3129  	})
  3130  	c.Assert(results, HasLen, 1)
  3131  	c.Assert(results[0].InstanceName(), Equals, "foo-2")
  3132  	c.Assert(results[0].SnapID, Equals, "foo-2-id")
  3133  }
  3134  
  3135  func (s *storeActionSuite) TestSnapAction500(c *C) {
  3136  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3137  		assertRequest(c, r, "POST", snapActionPath)
  3138  		// check device authorization is set, implicitly checking doRequest was used
  3139  		w.WriteHeader(500)
  3140  	}))
  3141  
  3142  	c.Assert(mockServer, NotNil)
  3143  	defer mockServer.Close()
  3144  
  3145  	mockServerURL, _ := url.Parse(mockServer.URL)
  3146  	cfg := store.Config{
  3147  		StoreBaseURL: mockServerURL,
  3148  	}
  3149  	dauthCtx := &testDauthContext{c: c, device: s.device}
  3150  	sto := store.New(&cfg, dauthCtx)
  3151  
  3152  	results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{
  3153  		{
  3154  			Action:       "install",
  3155  			InstanceName: "foo",
  3156  		},
  3157  	}, nil, nil, nil)
  3158  	c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://127\.0\.0\.1:.*/v2/snaps/refresh"`)
  3159  	c.Check(err, FitsTypeOf, &store.UnexpectedHTTPStatusError{})
  3160  	c.Check(results, HasLen, 0)
  3161  }
  3162  
  3163  func (s *storeActionSuite) TestSnapAction400(c *C) {
  3164  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  3165  		assertRequest(c, r, "POST", snapActionPath)
  3166  		// check device authorization is set, implicitly checking doRequest was used
  3167  		w.WriteHeader(400)
  3168  	}))
  3169  
  3170  	c.Assert(mockServer, NotNil)
  3171  	defer mockServer.Close()
  3172  
  3173  	mockServerURL, _ := url.Parse(mockServer.URL)
  3174  	cfg := store.Config{
  3175  		StoreBaseURL: mockServerURL,
  3176  	}
  3177  	dauthCtx := &testDauthContext{c: c, device: s.device}
  3178  	sto := store.New(&cfg, dauthCtx)
  3179  
  3180  	results, _, err := sto.SnapAction(s.ctx, nil, []*store.SnapAction{
  3181  		{
  3182  			Action:       "install",
  3183  			InstanceName: "foo",
  3184  		},
  3185  	}, nil, nil, nil)
  3186  	c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 400 via POST to "http://127\.0\.0\.1:.*/v2/snaps/refresh"`)
  3187  	c.Check(err, FitsTypeOf, &store.UnexpectedHTTPStatusError{})
  3188  	c.Check(results, HasLen, 0)
  3189  }