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