github.com/rigado/snapd@v2.42.5-go-mod+incompatible/client/snap_op_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 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 client_test
    21  
    22  import (
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"mime"
    29  	"mime/multipart"
    30  	"path/filepath"
    31  
    32  	"gopkg.in/check.v1"
    33  
    34  	"github.com/snapcore/snapd/client"
    35  )
    36  
    37  var chanName = "achan"
    38  
    39  var ops = []struct {
    40  	op     func(*client.Client, string, *client.SnapOptions) (string, error)
    41  	action string
    42  }{
    43  	{(*client.Client).Install, "install"},
    44  	{(*client.Client).Refresh, "refresh"},
    45  	{(*client.Client).Remove, "remove"},
    46  	{(*client.Client).Revert, "revert"},
    47  	{(*client.Client).Enable, "enable"},
    48  	{(*client.Client).Disable, "disable"},
    49  	{(*client.Client).Switch, "switch"},
    50  }
    51  
    52  var multiOps = []struct {
    53  	op     func(*client.Client, []string, *client.SnapOptions) (string, error)
    54  	action string
    55  }{
    56  	{(*client.Client).RefreshMany, "refresh"},
    57  	{(*client.Client).InstallMany, "install"},
    58  	{(*client.Client).RemoveMany, "remove"},
    59  }
    60  
    61  func (cs *clientSuite) TestClientOpSnapServerError(c *check.C) {
    62  	cs.err = errors.New("fail")
    63  	for _, s := range ops {
    64  		_, err := s.op(cs.cli, pkgName, nil)
    65  		c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action))
    66  	}
    67  }
    68  
    69  func (cs *clientSuite) TestClientMultiOpSnapServerError(c *check.C) {
    70  	cs.err = errors.New("fail")
    71  	for _, s := range multiOps {
    72  		_, err := s.op(cs.cli, nil, nil)
    73  		c.Check(err, check.ErrorMatches, `.*fail`, check.Commentf(s.action))
    74  	}
    75  	_, _, err := cs.cli.SnapshotMany(nil, nil)
    76  	c.Check(err, check.ErrorMatches, `.*fail`)
    77  }
    78  
    79  func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) {
    80  	cs.status = 400
    81  	cs.rsp = `{"type": "error"}`
    82  	for _, s := range ops {
    83  		_, err := s.op(cs.cli, pkgName, nil)
    84  		c.Check(err, check.ErrorMatches, `.*server error: "Bad Request"`, check.Commentf(s.action))
    85  	}
    86  }
    87  
    88  func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) {
    89  	cs.status = 500
    90  	cs.rsp = `{"type": "error"}`
    91  	for _, s := range multiOps {
    92  		_, err := s.op(cs.cli, nil, nil)
    93  		c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`, check.Commentf(s.action))
    94  	}
    95  	_, _, err := cs.cli.SnapshotMany(nil, nil)
    96  	c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`)
    97  }
    98  
    99  func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) {
   100  	cs.rsp = `{"type": "what"}`
   101  	for _, s := range ops {
   102  		_, err := s.op(cs.cli, pkgName, nil)
   103  		c.Check(err, check.ErrorMatches, `.*expected async response for "POST" on "/v2/snaps/`+pkgName+`", got "what"`, check.Commentf(s.action))
   104  	}
   105  }
   106  
   107  func (cs *clientSuite) TestClientOpSnapNotAccepted(c *check.C) {
   108  	cs.rsp = `{
   109  		"status-code": 200,
   110  		"type": "async"
   111  	}`
   112  	for _, s := range ops {
   113  		_, err := s.op(cs.cli, pkgName, nil)
   114  		c.Check(err, check.ErrorMatches, `.*operation not accepted`, check.Commentf(s.action))
   115  	}
   116  }
   117  
   118  func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) {
   119  	cs.status = 202
   120  	cs.rsp = `{
   121  		"status-code": 202,
   122  		"type": "async"
   123  	}`
   124  	for _, s := range ops {
   125  		_, err := s.op(cs.cli, pkgName, nil)
   126  		c.Assert(err, check.ErrorMatches, `.*response without change reference.*`, check.Commentf(s.action))
   127  	}
   128  }
   129  
   130  func (cs *clientSuite) TestClientOpSnap(c *check.C) {
   131  	cs.status = 202
   132  	cs.rsp = `{
   133  		"change": "d728",
   134  		"status-code": 202,
   135  		"type": "async"
   136  	}`
   137  	for _, s := range ops {
   138  		id, err := s.op(cs.cli, pkgName, &client.SnapOptions{Channel: chanName})
   139  		c.Assert(err, check.IsNil)
   140  
   141  		c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action))
   142  
   143  		body, err := ioutil.ReadAll(cs.req.Body)
   144  		c.Assert(err, check.IsNil, check.Commentf(s.action))
   145  		jsonBody := make(map[string]string)
   146  		err = json.Unmarshal(body, &jsonBody)
   147  		c.Assert(err, check.IsNil, check.Commentf(s.action))
   148  		c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action))
   149  		c.Check(jsonBody["channel"], check.Equals, chanName, check.Commentf(s.action))
   150  		c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action))
   151  
   152  		c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps/%s", pkgName), check.Commentf(s.action))
   153  		c.Check(id, check.Equals, "d728", check.Commentf(s.action))
   154  	}
   155  }
   156  
   157  func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) {
   158  	cs.status = 202
   159  	cs.rsp = `{
   160  		"change": "d728",
   161  		"status-code": 202,
   162  		"type": "async"
   163  	}`
   164  	for _, s := range multiOps {
   165  		// Note body is essentially the same as TestClientMultiSnapshot; keep in sync
   166  		id, err := s.op(cs.cli, []string{pkgName}, nil)
   167  		c.Assert(err, check.IsNil)
   168  
   169  		c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action))
   170  
   171  		body, err := ioutil.ReadAll(cs.req.Body)
   172  		c.Assert(err, check.IsNil, check.Commentf(s.action))
   173  		jsonBody := make(map[string]interface{})
   174  		err = json.Unmarshal(body, &jsonBody)
   175  		c.Assert(err, check.IsNil, check.Commentf(s.action))
   176  		c.Check(jsonBody["action"], check.Equals, s.action, check.Commentf(s.action))
   177  		c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName}, check.Commentf(s.action))
   178  		c.Check(jsonBody, check.HasLen, 2, check.Commentf(s.action))
   179  
   180  		c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps", check.Commentf(s.action))
   181  		c.Check(id, check.Equals, "d728", check.Commentf(s.action))
   182  	}
   183  }
   184  
   185  func (cs *clientSuite) TestClientMultiSnapshot(c *check.C) {
   186  	// Note body is essentially the same as TestClientMultiOpSnap; keep in sync
   187  	cs.status = 202
   188  	cs.rsp = `{
   189                  "result": {"set-id": 42},
   190  		"change": "d728",
   191  		"status-code": 202,
   192  		"type": "async"
   193  	}`
   194  	setID, changeID, err := cs.cli.SnapshotMany([]string{pkgName}, nil)
   195  	c.Assert(err, check.IsNil)
   196  	c.Check(cs.req.Header.Get("Content-Type"), check.Equals, "application/json")
   197  
   198  	body, err := ioutil.ReadAll(cs.req.Body)
   199  	c.Assert(err, check.IsNil)
   200  	jsonBody := make(map[string]interface{})
   201  	err = json.Unmarshal(body, &jsonBody)
   202  	c.Assert(err, check.IsNil)
   203  	c.Check(jsonBody["action"], check.Equals, "snapshot")
   204  	c.Check(jsonBody["snaps"], check.DeepEquals, []interface{}{pkgName})
   205  	c.Check(jsonBody, check.HasLen, 2)
   206  	c.Check(cs.req.URL.Path, check.Equals, "/v2/snaps")
   207  	c.Check(setID, check.Equals, uint64(42))
   208  	c.Check(changeID, check.Equals, "d728")
   209  }
   210  
   211  func (cs *clientSuite) TestClientOpInstallPath(c *check.C) {
   212  	cs.status = 202
   213  	cs.rsp = `{
   214  		"change": "66b3",
   215  		"status-code": 202,
   216  		"type": "async"
   217  	}`
   218  	bodyData := []byte("snap-data")
   219  
   220  	snap := filepath.Join(c.MkDir(), "foo.snap")
   221  	err := ioutil.WriteFile(snap, bodyData, 0644)
   222  	c.Assert(err, check.IsNil)
   223  
   224  	id, err := cs.cli.InstallPath(snap, "", nil)
   225  	c.Assert(err, check.IsNil)
   226  
   227  	body, err := ioutil.ReadAll(cs.req.Body)
   228  	c.Assert(err, check.IsNil)
   229  
   230  	c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
   231  	c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
   232  
   233  	c.Check(cs.req.Method, check.Equals, "POST")
   234  	c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"))
   235  	c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*")
   236  	c.Check(id, check.Equals, "66b3")
   237  }
   238  
   239  func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) {
   240  	cs.status = 202
   241  	cs.rsp = `{
   242  		"change": "66b3",
   243  		"status-code": 202,
   244  		"type": "async"
   245  	}`
   246  	bodyData := []byte("snap-data")
   247  
   248  	snap := filepath.Join(c.MkDir(), "foo.snap")
   249  	err := ioutil.WriteFile(snap, bodyData, 0644)
   250  	c.Assert(err, check.IsNil)
   251  
   252  	id, err := cs.cli.InstallPath(snap, "foo_bar", nil)
   253  	c.Assert(err, check.IsNil)
   254  
   255  	body, err := ioutil.ReadAll(cs.req.Body)
   256  	c.Assert(err, check.IsNil)
   257  
   258  	c.Assert(string(body), check.Matches, "(?s).*\r\nsnap-data\r\n.*")
   259  	c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"action\"\r\n\r\ninstall\r\n.*")
   260  	c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"name\"\r\n\r\nfoo_bar\r\n.*")
   261  
   262  	c.Check(cs.req.Method, check.Equals, "POST")
   263  	c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"))
   264  	c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*")
   265  	c.Check(id, check.Equals, "66b3")
   266  }
   267  
   268  func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) {
   269  	cs.status = 202
   270  	cs.rsp = `{
   271  		"change": "66b3",
   272  		"status-code": 202,
   273  		"type": "async"
   274  	}`
   275  	bodyData := []byte("snap-data")
   276  
   277  	snap := filepath.Join(c.MkDir(), "foo.snap")
   278  	err := ioutil.WriteFile(snap, bodyData, 0644)
   279  	c.Assert(err, check.IsNil)
   280  
   281  	opts := client.SnapOptions{
   282  		Dangerous: true,
   283  	}
   284  
   285  	// InstallPath takes Dangerous
   286  	_, err = cs.cli.InstallPath(snap, "", &opts)
   287  	c.Assert(err, check.IsNil)
   288  
   289  	body, err := ioutil.ReadAll(cs.req.Body)
   290  	c.Assert(err, check.IsNil)
   291  
   292  	c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"dangerous\"\r\n\r\ntrue\r\n.*")
   293  
   294  	// Install does not (and gives us a clear error message)
   295  	_, err = cs.cli.Install("foo", &opts)
   296  	c.Assert(err, check.Equals, client.ErrDangerousNotApplicable)
   297  
   298  	// nor does InstallMany (whether it fails because any option
   299  	// at all was provided, or because dangerous was provided, is
   300  	// unimportant)
   301  	_, err = cs.cli.InstallMany([]string{"foo"}, &opts)
   302  	c.Assert(err, check.NotNil)
   303  }
   304  
   305  func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) {
   306  	cs.status = 202
   307  	cs.rsp = `{
   308  		"change": "66b3",
   309  		"status-code": 202,
   310  		"type": "async"
   311  	}`
   312  	bodyData := []byte("snap-data")
   313  
   314  	snap := filepath.Join(c.MkDir(), "foo.snap")
   315  	err := ioutil.WriteFile(snap, bodyData, 0644)
   316  	c.Assert(err, check.IsNil)
   317  
   318  	opts := client.SnapOptions{
   319  		Unaliased: true,
   320  	}
   321  
   322  	_, err = cs.cli.Install("foo", &opts)
   323  	c.Assert(err, check.IsNil)
   324  
   325  	body, err := ioutil.ReadAll(cs.req.Body)
   326  	c.Assert(err, check.IsNil)
   327  	jsonBody := make(map[string]interface{})
   328  	err = json.Unmarshal(body, &jsonBody)
   329  	c.Assert(err, check.IsNil, check.Commentf("body: %v", string(body)))
   330  	c.Check(jsonBody["unaliased"], check.Equals, true, check.Commentf("body: %v", string(body)))
   331  
   332  	_, err = cs.cli.InstallPath(snap, "", &opts)
   333  	c.Assert(err, check.IsNil)
   334  
   335  	body, err = ioutil.ReadAll(cs.req.Body)
   336  	c.Assert(err, check.IsNil)
   337  
   338  	c.Assert(string(body), check.Matches, "(?s).*Content-Disposition: form-data; name=\"unaliased\"\r\n\r\ntrue\r\n.*")
   339  }
   340  
   341  func formToMap(c *check.C, mr *multipart.Reader) map[string]string {
   342  	formData := map[string]string{}
   343  	for {
   344  		p, err := mr.NextPart()
   345  		if err == io.EOF {
   346  			break
   347  		}
   348  		c.Assert(err, check.IsNil)
   349  		slurp, err := ioutil.ReadAll(p)
   350  		c.Assert(err, check.IsNil)
   351  		formData[p.FormName()] = string(slurp)
   352  	}
   353  	return formData
   354  }
   355  
   356  func (cs *clientSuite) TestClientOpTryMode(c *check.C) {
   357  	cs.status = 202
   358  	cs.rsp = `{
   359  		"change": "66b3",
   360  		"status-code": 202,
   361  		"type": "async"
   362  	}`
   363  	snapdir := filepath.Join(c.MkDir(), "/some/path")
   364  
   365  	for _, opts := range []*client.SnapOptions{
   366  		{Classic: false, DevMode: false, JailMode: false},
   367  		{Classic: false, DevMode: false, JailMode: true},
   368  		{Classic: false, DevMode: true, JailMode: true},
   369  		{Classic: false, DevMode: true, JailMode: false},
   370  		{Classic: true, DevMode: false, JailMode: false},
   371  		{Classic: true, DevMode: false, JailMode: true},
   372  		{Classic: true, DevMode: true, JailMode: true},
   373  		{Classic: true, DevMode: true, JailMode: false},
   374  	} {
   375  		comment := check.Commentf("when Classic:%t DevMode:%t JailMode:%t", opts.Classic, opts.DevMode, opts.JailMode)
   376  		id, err := cs.cli.Try(snapdir, opts)
   377  		c.Assert(err, check.IsNil)
   378  
   379  		// ensure we send the right form-data
   380  		_, params, err := mime.ParseMediaType(cs.req.Header.Get("Content-Type"))
   381  		c.Assert(err, check.IsNil, comment)
   382  		mr := multipart.NewReader(cs.req.Body, params["boundary"])
   383  		formData := formToMap(c, mr)
   384  		c.Check(formData["action"], check.Equals, "try", comment)
   385  		c.Check(formData["snap-path"], check.Equals, snapdir, comment)
   386  		expectedLength := 2
   387  		if opts.Classic {
   388  			c.Check(formData["classic"], check.Equals, "true", comment)
   389  			expectedLength++
   390  		}
   391  		if opts.DevMode {
   392  			c.Check(formData["devmode"], check.Equals, "true", comment)
   393  			expectedLength++
   394  		}
   395  		if opts.JailMode {
   396  			c.Check(formData["jailmode"], check.Equals, "true", comment)
   397  			expectedLength++
   398  		}
   399  		c.Check(len(formData), check.Equals, expectedLength)
   400  
   401  		c.Check(cs.req.Method, check.Equals, "POST", comment)
   402  		c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps"), comment)
   403  		c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*", comment)
   404  		c.Check(id, check.Equals, "66b3", comment)
   405  	}
   406  }
   407  
   408  func (cs *clientSuite) TestClientOpTryModeDangerous(c *check.C) {
   409  	snapdir := filepath.Join(c.MkDir(), "/some/path")
   410  
   411  	_, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true})
   412  	c.Assert(err, check.Equals, client.ErrDangerousNotApplicable)
   413  }
   414  
   415  func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) {
   416  	tests := map[string]client.SnapOptions{
   417  		"{}":                         {},
   418  		`{"channel":"edge"}`:         {Channel: "edge"},
   419  		`{"revision":"42"}`:          {Revision: "42"},
   420  		`{"cohort-key":"what"}`:      {CohortKey: "what"},
   421  		`{"leave-cohort":true}`:      {LeaveCohort: true},
   422  		`{"devmode":true}`:           {DevMode: true},
   423  		`{"jailmode":true}`:          {JailMode: true},
   424  		`{"classic":true}`:           {Classic: true},
   425  		`{"dangerous":true}`:         {Dangerous: true},
   426  		`{"ignore-validation":true}`: {IgnoreValidation: true},
   427  		`{"unaliased":true}`:         {Unaliased: true},
   428  		`{"purge":true}`:             {Purge: true},
   429  		`{"amend":true}`:             {Amend: true},
   430  	}
   431  	for expected, opts := range tests {
   432  		buf, err := json.Marshal(&opts)
   433  		c.Assert(err, check.IsNil, check.Commentf("%s", expected))
   434  		c.Check(string(buf), check.Equals, expected)
   435  	}
   436  }