github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/daemon/api_sideload_n_try_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package daemon_test
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"net/http"
    28  	"os"
    29  	"path/filepath"
    30  	"regexp"
    31  	"time"
    32  
    33  	"gopkg.in/check.v1"
    34  
    35  	"github.com/snapcore/snapd/asserts"
    36  	"github.com/snapcore/snapd/asserts/assertstest"
    37  	"github.com/snapcore/snapd/client"
    38  	"github.com/snapcore/snapd/daemon"
    39  	"github.com/snapcore/snapd/dirs"
    40  	"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
    41  	"github.com/snapcore/snapd/overlord/snapstate"
    42  	"github.com/snapcore/snapd/overlord/state"
    43  	"github.com/snapcore/snapd/sandbox"
    44  	"github.com/snapcore/snapd/snap"
    45  	"github.com/snapcore/snapd/testutil"
    46  )
    47  
    48  var (
    49  	_ = check.Suite(&sideloadSuite{})
    50  	_ = check.Suite(&trySuite{})
    51  )
    52  
    53  type sideloadSuite struct {
    54  	apiBaseSuite
    55  }
    56  
    57  var sideLoadBodyWithoutDevMode = "" +
    58  	"----hello--\r\n" +
    59  	"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
    60  	"\r\n" +
    61  	"xyzzy\r\n" +
    62  	"----hello--\r\n" +
    63  	"Content-Disposition: form-data; name=\"dangerous\"\r\n" +
    64  	"\r\n" +
    65  	"true\r\n" +
    66  	"----hello--\r\n" +
    67  	"Content-Disposition: form-data; name=\"snap-path\"\r\n" +
    68  	"\r\n" +
    69  	"a/b/local.snap\r\n" +
    70  	"----hello--\r\n"
    71  
    72  func (s *sideloadSuite) TestSideloadSnapOnNonDevModeDistro(c *check.C) {
    73  	// try a multipart/form-data upload
    74  	body := sideLoadBodyWithoutDevMode
    75  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
    76  	chgSummary := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true})
    77  	c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`)
    78  }
    79  
    80  func (s *sideloadSuite) TestSideloadSnapOnDevModeDistro(c *check.C) {
    81  	// try a multipart/form-data upload
    82  	body := sideLoadBodyWithoutDevMode
    83  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
    84  	restore := sandbox.MockForceDevMode(true)
    85  	defer restore()
    86  	flags := snapstate.Flags{RemoveSnapPath: true}
    87  	chgSummary := s.sideloadCheck(c, body, head, "local", flags)
    88  	c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`)
    89  }
    90  
    91  func (s *sideloadSuite) TestSideloadSnapDevMode(c *check.C) {
    92  	body := "" +
    93  		"----hello--\r\n" +
    94  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
    95  		"\r\n" +
    96  		"xyzzy\r\n" +
    97  		"----hello--\r\n" +
    98  		"Content-Disposition: form-data; name=\"devmode\"\r\n" +
    99  		"\r\n" +
   100  		"true\r\n" +
   101  		"----hello--\r\n"
   102  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
   103  	// try a multipart/form-data upload
   104  	flags := snapstate.Flags{RemoveSnapPath: true}
   105  	flags.DevMode = true
   106  	chgSummary := s.sideloadCheck(c, body, head, "local", flags)
   107  	c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`)
   108  }
   109  
   110  func (s *sideloadSuite) TestSideloadSnapJailMode(c *check.C) {
   111  	body := "" +
   112  		"----hello--\r\n" +
   113  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
   114  		"\r\n" +
   115  		"xyzzy\r\n" +
   116  		"----hello--\r\n" +
   117  		"Content-Disposition: form-data; name=\"jailmode\"\r\n" +
   118  		"\r\n" +
   119  		"true\r\n" +
   120  		"----hello--\r\n" +
   121  		"Content-Disposition: form-data; name=\"dangerous\"\r\n" +
   122  		"\r\n" +
   123  		"true\r\n" +
   124  		"----hello--\r\n"
   125  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
   126  	// try a multipart/form-data upload
   127  	flags := snapstate.Flags{JailMode: true, RemoveSnapPath: true}
   128  	chgSummary := s.sideloadCheck(c, body, head, "local", flags)
   129  	c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`)
   130  }
   131  
   132  func (s *sideloadSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedInstanceName string, expectedFlags snapstate.Flags) string {
   133  	d := s.daemonWithFakeSnapManager(c)
   134  
   135  	soon := 0
   136  	var origEnsureStateSoon func(*state.State)
   137  	origEnsureStateSoon, restore := daemon.MockEnsureStateSoon(func(st *state.State) {
   138  		soon++
   139  		origEnsureStateSoon(st)
   140  	})
   141  	defer restore()
   142  
   143  	c.Assert(expectedInstanceName != "", check.Equals, true, check.Commentf("expected instance name must be set"))
   144  	mockedName, _ := snap.SplitInstanceName(expectedInstanceName)
   145  
   146  	// setup done
   147  	installQueue := []string{}
   148  	defer daemon.MockUnsafeReadSnapInfo(func(path string) (*snap.Info, error) {
   149  		return &snap.Info{SuggestedName: mockedName}, nil
   150  	})()
   151  
   152  	defer daemon.MockSnapstateInstall(func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
   153  		// NOTE: ubuntu-core is not installed in developer mode
   154  		c.Check(flags, check.Equals, snapstate.Flags{})
   155  		installQueue = append(installQueue, name)
   156  
   157  		t := s.NewTask("fake-install-snap", "Doing a fake install")
   158  		return state.NewTaskSet(t), nil
   159  	})()
   160  
   161  	defer daemon.MockSnapstateInstallPath(func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) {
   162  		c.Check(flags, check.DeepEquals, expectedFlags)
   163  
   164  		c.Check(path, testutil.FileEquals, "xyzzy")
   165  
   166  		c.Check(name, check.Equals, expectedInstanceName)
   167  
   168  		installQueue = append(installQueue, si.RealName+"::"+path)
   169  		t := s.NewTask("fake-install-snap", "Doing a fake install")
   170  		return state.NewTaskSet(t), &snap.Info{SuggestedName: name}, nil
   171  	})()
   172  
   173  	buf := bytes.NewBufferString(content)
   174  	req, err := http.NewRequest("POST", "/v2/snaps", buf)
   175  	c.Assert(err, check.IsNil)
   176  	for k, v := range head {
   177  		req.Header.Set(k, v)
   178  	}
   179  
   180  	rsp := s.asyncReq(c, req, nil)
   181  	n := 1
   182  	c.Assert(installQueue, check.HasLen, n)
   183  	c.Check(installQueue[n-1], check.Matches, "local::.*/"+regexp.QuoteMeta(dirs.LocalInstallBlobTempPrefix)+".*")
   184  
   185  	st := d.Overlord().State()
   186  	st.Lock()
   187  	defer st.Unlock()
   188  	chg := st.Change(rsp.Change)
   189  	c.Assert(chg, check.NotNil)
   190  
   191  	c.Check(soon, check.Equals, 1)
   192  
   193  	c.Assert(chg.Tasks(), check.HasLen, n)
   194  
   195  	st.Unlock()
   196  	s.waitTrivialChange(c, chg)
   197  	st.Lock()
   198  
   199  	c.Check(chg.Kind(), check.Equals, "install-snap")
   200  	var names []string
   201  	err = chg.Get("snap-names", &names)
   202  	c.Assert(err, check.IsNil)
   203  	c.Check(names, check.DeepEquals, []string{expectedInstanceName})
   204  	var apiData map[string]interface{}
   205  	err = chg.Get("api-data", &apiData)
   206  	c.Assert(err, check.IsNil)
   207  	c.Check(apiData, check.DeepEquals, map[string]interface{}{
   208  		"snap-name": expectedInstanceName,
   209  	})
   210  
   211  	return chg.Summary()
   212  }
   213  
   214  func (s *sideloadSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) {
   215  	body := "" +
   216  		"----hello--\r\n" +
   217  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
   218  		"\r\n" +
   219  		"xyzzy\r\n" +
   220  		"----hello--\r\n" +
   221  		"Content-Disposition: form-data; name=\"jailmode\"\r\n" +
   222  		"\r\n" +
   223  		"true\r\n" +
   224  		"----hello--\r\n" +
   225  		"Content-Disposition: form-data; name=\"devmode\"\r\n" +
   226  		"\r\n" +
   227  		"true\r\n" +
   228  		"----hello--\r\n"
   229  	s.daemonWithOverlordMockAndStore(c)
   230  
   231  	req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
   232  	c.Assert(err, check.IsNil)
   233  	req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
   234  
   235  	rsp := s.errorReq(c, req, nil)
   236  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, "cannot use devmode and jailmode flags together")
   237  }
   238  
   239  func (s *sideloadSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) {
   240  	body := "" +
   241  		"----hello--\r\n" +
   242  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
   243  		"\r\n" +
   244  		"xyzzy\r\n" +
   245  		"----hello--\r\n" +
   246  		"Content-Disposition: form-data; name=\"jailmode\"\r\n" +
   247  		"\r\n" +
   248  		"true\r\n" +
   249  		"----hello--\r\n"
   250  	s.daemonWithOverlordMockAndStore(c)
   251  
   252  	req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
   253  	c.Assert(err, check.IsNil)
   254  	req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
   255  
   256  	restore := sandbox.MockForceDevMode(true)
   257  	defer restore()
   258  
   259  	rsp := s.errorReq(c, req, nil)
   260  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, "this system cannot honour the jailmode flag")
   261  }
   262  
   263  func (s *sideloadSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) {
   264  	d := s.daemonWithOverlordMockAndStore(c)
   265  	// add the assertions first
   266  	st := d.Overlord().State()
   267  
   268  	dev1Acct := assertstest.NewAccount(s.StoreSigning, "devel1", nil, "")
   269  
   270  	snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
   271  		"series":       "16",
   272  		"snap-id":      "x-id",
   273  		"snap-name":    "x",
   274  		"publisher-id": dev1Acct.AccountID(),
   275  		"timestamp":    time.Now().Format(time.RFC3339),
   276  	}, nil, "")
   277  	c.Assert(err, check.IsNil)
   278  
   279  	snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
   280  		"snap-sha3-384": "YK0GWATaZf09g_fvspYPqm_qtaiqf-KjaNj5uMEQCjQpuXWPjqQbeBINL5H_A0Lo",
   281  		"snap-size":     "5",
   282  		"snap-id":       "x-id",
   283  		"snap-revision": "41",
   284  		"developer-id":  dev1Acct.AccountID(),
   285  		"timestamp":     time.Now().Format(time.RFC3339),
   286  	}, nil, "")
   287  	c.Assert(err, check.IsNil)
   288  
   289  	func() {
   290  		st.Lock()
   291  		defer st.Unlock()
   292  		assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), dev1Acct, snapDecl, snapRev)
   293  	}()
   294  
   295  	body := "" +
   296  		"----hello--\r\n" +
   297  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x.snap\"\r\n" +
   298  		"\r\n" +
   299  		"xyzzy\r\n" +
   300  		"----hello--\r\n"
   301  	req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
   302  	c.Assert(err, check.IsNil)
   303  	req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
   304  
   305  	defer daemon.MockSnapstateInstallPath(func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) {
   306  		c.Check(flags, check.Equals, snapstate.Flags{RemoveSnapPath: true})
   307  		c.Check(si, check.DeepEquals, &snap.SideInfo{
   308  			RealName: "x",
   309  			SnapID:   "x-id",
   310  			Revision: snap.R(41),
   311  		})
   312  
   313  		return state.NewTaskSet(), &snap.Info{SuggestedName: "x"}, nil
   314  	})()
   315  
   316  	rsp := s.asyncReq(c, req, nil)
   317  
   318  	st.Lock()
   319  	defer st.Unlock()
   320  	chg := st.Change(rsp.Change)
   321  	c.Assert(chg, check.NotNil)
   322  	c.Check(chg.Summary(), check.Equals, `Install "x" snap from file "x.snap"`)
   323  	var names []string
   324  	err = chg.Get("snap-names", &names)
   325  	c.Assert(err, check.IsNil)
   326  	c.Check(names, check.DeepEquals, []string{"x"})
   327  	var apiData map[string]interface{}
   328  	err = chg.Get("api-data", &apiData)
   329  	c.Assert(err, check.IsNil)
   330  	c.Check(apiData, check.DeepEquals, map[string]interface{}{
   331  		"snap-name": "x",
   332  	})
   333  }
   334  
   335  func (s *sideloadSuite) TestSideloadSnapNoSignaturesDangerOff(c *check.C) {
   336  	body := "" +
   337  		"----hello--\r\n" +
   338  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
   339  		"\r\n" +
   340  		"xyzzy\r\n" +
   341  		"----hello--\r\n"
   342  	s.daemonWithOverlordMockAndStore(c)
   343  
   344  	req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
   345  	c.Assert(err, check.IsNil)
   346  	req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
   347  
   348  	// this is the prefix used for tempfiles for sideloading
   349  	glob := filepath.Join(os.TempDir(), "snapd-sideload-pkg-*")
   350  	glbBefore, _ := filepath.Glob(glob)
   351  	rsp := s.errorReq(c, req, nil)
   352  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `cannot find signatures with metadata for snap "x"`)
   353  	glbAfter, _ := filepath.Glob(glob)
   354  	c.Check(len(glbBefore), check.Equals, len(glbAfter))
   355  }
   356  
   357  func (s *sideloadSuite) TestSideloadSnapNotValidFormFile(c *check.C) {
   358  	s.daemon(c)
   359  
   360  	// try a multipart/form-data upload with missing "name"
   361  	content := "" +
   362  		"----hello--\r\n" +
   363  		"Content-Disposition: form-data; filename=\"x\"\r\n" +
   364  		"\r\n" +
   365  		"xyzzy\r\n" +
   366  		"----hello--\r\n"
   367  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
   368  
   369  	buf := bytes.NewBufferString(content)
   370  	req, err := http.NewRequest("POST", "/v2/snaps", buf)
   371  	c.Assert(err, check.IsNil)
   372  	for k, v := range head {
   373  		req.Header.Set(k, v)
   374  	}
   375  
   376  	rsp := s.errorReq(c, req, nil)
   377  	c.Assert(rsp.Result.(*daemon.ErrorResult).Message, check.Matches, `cannot find "snap" file field in provided multipart/form-data payload`)
   378  }
   379  
   380  func (s *sideloadSuite) TestSideloadSnapChangeConflict(c *check.C) {
   381  	body := "" +
   382  		"----hello--\r\n" +
   383  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
   384  		"\r\n" +
   385  		"xyzzy\r\n" +
   386  		"----hello--\r\n" +
   387  		"Content-Disposition: form-data; name=\"dangerous\"\r\n" +
   388  		"\r\n" +
   389  		"true\r\n" +
   390  		"----hello--\r\n"
   391  	s.daemonWithOverlordMockAndStore(c)
   392  
   393  	defer daemon.MockUnsafeReadSnapInfo(func(path string) (*snap.Info, error) {
   394  		return &snap.Info{SuggestedName: "foo"}, nil
   395  	})()
   396  
   397  	defer daemon.MockSnapstateInstallPath(func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) {
   398  		return nil, nil, &snapstate.ChangeConflictError{Snap: "foo"}
   399  	})()
   400  
   401  	req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
   402  	c.Assert(err, check.IsNil)
   403  	req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
   404  
   405  	rsp := s.errorReq(c, req, nil)
   406  	c.Check(rsp.Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindSnapChangeConflict)
   407  }
   408  
   409  func (s *sideloadSuite) TestSideloadSnapInstanceName(c *check.C) {
   410  	// try a multipart/form-data upload
   411  	body := sideLoadBodyWithoutDevMode +
   412  		"Content-Disposition: form-data; name=\"name\"\r\n" +
   413  		"\r\n" +
   414  		"local_instance\r\n" +
   415  		"----hello--\r\n"
   416  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
   417  	chgSummary := s.sideloadCheck(c, body, head, "local_instance", snapstate.Flags{RemoveSnapPath: true})
   418  	c.Check(chgSummary, check.Equals, `Install "local_instance" snap from file "a/b/local.snap"`)
   419  }
   420  
   421  func (s *sideloadSuite) TestSideloadSnapInstanceNameNoKey(c *check.C) {
   422  	// try a multipart/form-data upload
   423  	body := sideLoadBodyWithoutDevMode +
   424  		"Content-Disposition: form-data; name=\"name\"\r\n" +
   425  		"\r\n" +
   426  		"local\r\n" +
   427  		"----hello--\r\n"
   428  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
   429  	chgSummary := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true})
   430  	c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`)
   431  }
   432  
   433  func (s *sideloadSuite) TestSideloadSnapInstanceNameMismatch(c *check.C) {
   434  	s.daemonWithFakeSnapManager(c)
   435  
   436  	defer daemon.MockUnsafeReadSnapInfo(func(path string) (*snap.Info, error) {
   437  		return &snap.Info{SuggestedName: "bar"}, nil
   438  	})()
   439  
   440  	body := sideLoadBodyWithoutDevMode +
   441  		"Content-Disposition: form-data; name=\"name\"\r\n" +
   442  		"\r\n" +
   443  		"foo_instance\r\n" +
   444  		"----hello--\r\n"
   445  
   446  	req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body))
   447  	c.Assert(err, check.IsNil)
   448  	req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--")
   449  
   450  	rsp := s.errorReq(c, req, nil)
   451  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, check.Equals, `instance name "foo_instance" does not match snap name "bar"`)
   452  }
   453  
   454  func (s *sideloadSuite) TestInstallPathUnaliased(c *check.C) {
   455  	body := "" +
   456  		"----hello--\r\n" +
   457  		"Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" +
   458  		"\r\n" +
   459  		"xyzzy\r\n" +
   460  		"----hello--\r\n" +
   461  		"Content-Disposition: form-data; name=\"devmode\"\r\n" +
   462  		"\r\n" +
   463  		"true\r\n" +
   464  		"----hello--\r\n" +
   465  		"Content-Disposition: form-data; name=\"unaliased\"\r\n" +
   466  		"\r\n" +
   467  		"true\r\n" +
   468  		"----hello--\r\n"
   469  	head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"}
   470  	// try a multipart/form-data upload
   471  	flags := snapstate.Flags{Unaliased: true, RemoveSnapPath: true, DevMode: true}
   472  	chgSummary := s.sideloadCheck(c, body, head, "local", flags)
   473  	c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`)
   474  }
   475  
   476  type trySuite struct {
   477  	apiBaseSuite
   478  }
   479  
   480  func (s *trySuite) TestTrySnap(c *check.C) {
   481  	d := s.daemonWithFakeSnapManager(c)
   482  
   483  	var err error
   484  
   485  	// mock a try dir
   486  	tryDir := c.MkDir()
   487  	snapYaml := filepath.Join(tryDir, "meta", "snap.yaml")
   488  	err = os.MkdirAll(filepath.Dir(snapYaml), 0755)
   489  	c.Assert(err, check.IsNil)
   490  	err = ioutil.WriteFile(snapYaml, []byte("name: foo\nversion: 1.0\n"), 0644)
   491  	c.Assert(err, check.IsNil)
   492  
   493  	reqForFlags := func(f snapstate.Flags) *http.Request {
   494  		b := "" +
   495  			"--hello\r\n" +
   496  			"Content-Disposition: form-data; name=\"action\"\r\n" +
   497  			"\r\n" +
   498  			"try\r\n" +
   499  			"--hello\r\n" +
   500  			"Content-Disposition: form-data; name=\"snap-path\"\r\n" +
   501  			"\r\n" +
   502  			tryDir + "\r\n" +
   503  			"--hello"
   504  
   505  		snip := "\r\n" +
   506  			"Content-Disposition: form-data; name=%q\r\n" +
   507  			"\r\n" +
   508  			"true\r\n" +
   509  			"--hello"
   510  
   511  		if f.DevMode {
   512  			b += fmt.Sprintf(snip, "devmode")
   513  		}
   514  		if f.JailMode {
   515  			b += fmt.Sprintf(snip, "jailmode")
   516  		}
   517  		if f.Classic {
   518  			b += fmt.Sprintf(snip, "classic")
   519  		}
   520  		b += "--\r\n"
   521  
   522  		req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(b))
   523  		c.Assert(err, check.IsNil)
   524  		req.Header.Set("Content-Type", "multipart/thing; boundary=hello")
   525  
   526  		return req
   527  	}
   528  
   529  	st := d.Overlord().State()
   530  	st.Lock()
   531  	defer st.Unlock()
   532  
   533  	soon := 0
   534  	var origEnsureStateSoon func(*state.State)
   535  	origEnsureStateSoon, restore := daemon.MockEnsureStateSoon(func(st *state.State) {
   536  		soon++
   537  		origEnsureStateSoon(st)
   538  	})
   539  	defer restore()
   540  
   541  	for _, t := range []struct {
   542  		flags snapstate.Flags
   543  		desc  string
   544  	}{
   545  		{snapstate.Flags{}, "core; -"},
   546  		{snapstate.Flags{DevMode: true}, "core; devmode"},
   547  		{snapstate.Flags{JailMode: true}, "core; jailmode"},
   548  		{snapstate.Flags{Classic: true}, "core; classic"},
   549  	} {
   550  		soon = 0
   551  
   552  		tryWasCalled := true
   553  		defer daemon.MockSnapstateTryPath(func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) {
   554  			c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc))
   555  			tryWasCalled = true
   556  			t := s.NewTask("fake-install-snap", "Doing a fake try")
   557  			return state.NewTaskSet(t), nil
   558  		})()
   559  
   560  		defer daemon.MockSnapstateInstall(func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
   561  			if name != "core" {
   562  				c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc))
   563  			}
   564  			t := s.NewTask("fake-install-snap", "Doing a fake install")
   565  			return state.NewTaskSet(t), nil
   566  		})()
   567  
   568  		// try the snap (without an installed core)
   569  		st.Unlock()
   570  		rsp := s.asyncReq(c, reqForFlags(t.flags), nil)
   571  		st.Lock()
   572  		c.Assert(tryWasCalled, check.Equals, true, check.Commentf(t.desc))
   573  
   574  		chg := st.Change(rsp.Change)
   575  		c.Assert(chg, check.NotNil, check.Commentf(t.desc))
   576  
   577  		c.Assert(chg.Tasks(), check.HasLen, 1, check.Commentf(t.desc))
   578  
   579  		st.Unlock()
   580  		s.waitTrivialChange(c, chg)
   581  		st.Lock()
   582  
   583  		c.Check(chg.Kind(), check.Equals, "try-snap", check.Commentf(t.desc))
   584  		c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Try "%s" snap from %s`, "foo", tryDir), check.Commentf(t.desc))
   585  		var names []string
   586  		err = chg.Get("snap-names", &names)
   587  		c.Assert(err, check.IsNil, check.Commentf(t.desc))
   588  		c.Check(names, check.DeepEquals, []string{"foo"}, check.Commentf(t.desc))
   589  		var apiData map[string]interface{}
   590  		err = chg.Get("api-data", &apiData)
   591  		c.Assert(err, check.IsNil, check.Commentf(t.desc))
   592  		c.Check(apiData, check.DeepEquals, map[string]interface{}{
   593  			"snap-name": "foo",
   594  		}, check.Commentf(t.desc))
   595  
   596  		c.Check(soon, check.Equals, 1, check.Commentf(t.desc))
   597  	}
   598  }
   599  
   600  func (s *trySuite) TestTrySnapRelative(c *check.C) {
   601  	d := s.daemon(c)
   602  	st := d.Overlord().State()
   603  
   604  	rsp := daemon.TrySnap(st, "relative-path", snapstate.Flags{}).(*daemon.Resp)
   605  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   606  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, testutil.Contains, "need an absolute path")
   607  }
   608  
   609  func (s *trySuite) TestTrySnapNotDir(c *check.C) {
   610  	d := s.daemon(c)
   611  	st := d.Overlord().State()
   612  
   613  	rsp := daemon.TrySnap(st, "/does/not/exist", snapstate.Flags{}).(*daemon.Resp)
   614  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   615  	c.Check(rsp.Result.(*daemon.ErrorResult).Message, testutil.Contains, "not a snap directory")
   616  }
   617  
   618  func (s *trySuite) TestTryChangeConflict(c *check.C) {
   619  	d := s.daemonWithOverlordMockAndStore(c)
   620  	st := d.Overlord().State()
   621  
   622  	// mock a try dir
   623  	tryDir := c.MkDir()
   624  
   625  	defer daemon.MockUnsafeReadSnapInfo(func(path string) (*snap.Info, error) {
   626  		return &snap.Info{SuggestedName: "foo"}, nil
   627  	})()
   628  
   629  	defer daemon.MockSnapstateTryPath(func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) {
   630  		return nil, &snapstate.ChangeConflictError{Snap: "foo"}
   631  	})()
   632  
   633  	rsp := daemon.TrySnap(st, tryDir, snapstate.Flags{}).(*daemon.Resp)
   634  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   635  	c.Check(rsp.Result.(*daemon.ErrorResult).Kind, check.Equals, client.ErrorKindSnapChangeConflict)
   636  }