github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api_snapshots_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 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  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"strconv"
    31  	"strings"
    32  
    33  	"gopkg.in/check.v1"
    34  
    35  	"github.com/snapcore/snapd/client"
    36  	"github.com/snapcore/snapd/daemon"
    37  	"github.com/snapcore/snapd/overlord/snapshotstate"
    38  	"github.com/snapcore/snapd/overlord/state"
    39  )
    40  
    41  var _ = check.Suite(&snapshotSuite{})
    42  
    43  type snapshotSuite struct {
    44  	apiBaseSuite
    45  }
    46  
    47  func (s *snapshotSuite) SetUpTest(c *check.C) {
    48  	s.apiBaseSuite.SetUpTest(c)
    49  	s.daemonWithOverlordMock(c)
    50  }
    51  
    52  func (s *snapshotSuite) TestSnapshotMany(c *check.C) {
    53  	defer daemon.MockSnapshotSave(func(s *state.State, snaps, users []string) (uint64, []string, *state.TaskSet, error) {
    54  		c.Check(snaps, check.HasLen, 2)
    55  		t := s.NewTask("fake-snapshot-2", "Snapshot two")
    56  		return 1, snaps, state.NewTaskSet(t), nil
    57  	})()
    58  
    59  	inst := daemon.MustUnmarshalSnapInstruction(c, `{"action": "snapshot", "snaps": ["foo", "bar"]}`)
    60  	st := s.d.Overlord().State()
    61  	st.Lock()
    62  	res, err := daemon.SnapshotMany(inst, st)
    63  	st.Unlock()
    64  	c.Assert(err, check.IsNil)
    65  	c.Check(res.Summary, check.Equals, `Snapshot snaps "foo", "bar"`)
    66  	c.Check(res.Affected, check.DeepEquals, inst.Snaps)
    67  }
    68  
    69  func (s *snapshotSuite) TestListSnapshots(c *check.C) {
    70  	snapshots := []client.SnapshotSet{{ID: 1}, {ID: 42}}
    71  
    72  	defer daemon.MockSnapshotList(func(context.Context, *state.State, uint64, []string) ([]client.SnapshotSet, error) {
    73  		return snapshots, nil
    74  	})()
    75  
    76  	c.Check(daemon.SnapshotCmd.Path, check.Equals, "/v2/snapshots")
    77  	req, err := http.NewRequest("GET", "/v2/snapshots", nil)
    78  	c.Assert(err, check.IsNil)
    79  
    80  	rsp := s.req(c, req, nil).(*daemon.Resp)
    81  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
    82  	c.Check(rsp.Status, check.Equals, 200)
    83  	c.Check(rsp.Result, check.DeepEquals, snapshots)
    84  }
    85  
    86  func (s *snapshotSuite) TestListSnapshotsFiltering(c *check.C) {
    87  	snapshots := []client.SnapshotSet{{ID: 1}, {ID: 42}}
    88  
    89  	defer daemon.MockSnapshotList(func(_ context.Context, st *state.State, setID uint64, _ []string) ([]client.SnapshotSet, error) {
    90  		c.Assert(setID, check.Equals, uint64(42))
    91  		return snapshots[1:], nil
    92  	})()
    93  
    94  	req, err := http.NewRequest("GET", "/v2/snapshots?set=42", nil)
    95  	c.Assert(err, check.IsNil)
    96  
    97  	rsp := s.req(c, req, nil).(*daemon.Resp)
    98  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
    99  	c.Check(rsp.Status, check.Equals, 200)
   100  	c.Check(rsp.Result, check.DeepEquals, []client.SnapshotSet{{ID: 42}})
   101  }
   102  
   103  func (s *snapshotSuite) TestListSnapshotsBadFiltering(c *check.C) {
   104  	defer daemon.MockSnapshotList(func(_ context.Context, _ *state.State, setID uint64, _ []string) ([]client.SnapshotSet, error) {
   105  		c.Fatal("snapshotList should not be reached (should have been blocked by validation!)")
   106  		return nil, nil
   107  	})()
   108  
   109  	req, err := http.NewRequest("GET", "/v2/snapshots?set=no", nil)
   110  	c.Assert(err, check.IsNil)
   111  
   112  	rsp := s.req(c, req, nil).(*daemon.Resp)
   113  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   114  	c.Check(rsp.Status, check.Equals, 400)
   115  	c.Check(rsp.ErrorResult().Message, check.Equals, `'set', if given, must be a positive base 10 number; got "no"`)
   116  }
   117  
   118  func (s *snapshotSuite) TestListSnapshotsListError(c *check.C) {
   119  	defer daemon.MockSnapshotList(func(_ context.Context, _ *state.State, setID uint64, _ []string) ([]client.SnapshotSet, error) {
   120  		return nil, errors.New("no")
   121  	})()
   122  
   123  	c.Check(daemon.SnapshotCmd.Path, check.Equals, "/v2/snapshots")
   124  	req, err := http.NewRequest("GET", "/v2/snapshots", nil)
   125  	c.Assert(err, check.IsNil)
   126  
   127  	rsp := s.req(c, req, nil).(*daemon.Resp)
   128  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   129  	c.Check(rsp.Status, check.Equals, 500)
   130  	c.Check(rsp.ErrorResult().Message, check.Equals, "no")
   131  }
   132  
   133  func (s *snapshotSuite) TestFormatSnapshotAction(c *check.C) {
   134  	type table struct {
   135  		action   string
   136  		expected string
   137  	}
   138  	tests := []table{
   139  		{
   140  			`{"set": 2, "action": "verb"}`,
   141  			`Verb of snapshot set #2`,
   142  		}, {
   143  			`{"set": 2, "action": "verb", "snaps": ["foo"]}`,
   144  			`Verb of snapshot set #2 for snaps "foo"`,
   145  		}, {
   146  			`{"set": 2, "action": "verb", "snaps": ["foo", "bar"]}`,
   147  			`Verb of snapshot set #2 for snaps "foo", "bar"`,
   148  		}, {
   149  			`{"set": 2, "action": "verb", "users": ["meep"]}`,
   150  			`Verb of snapshot set #2 for users "meep"`,
   151  		}, {
   152  			`{"set": 2, "action": "verb", "users": ["meep", "quux"]}`,
   153  			`Verb of snapshot set #2 for users "meep", "quux"`,
   154  		}, {
   155  			`{"set": 2, "action": "verb", "users": ["meep", "quux"], "snaps": ["foo", "bar"]}`,
   156  			`Verb of snapshot set #2 for snaps "foo", "bar" for users "meep", "quux"`,
   157  		},
   158  	}
   159  
   160  	for _, test := range tests {
   161  		action := daemon.MustUnmarshalSnapshotAction(c, test.action)
   162  		c.Check(action.String(), check.Equals, test.expected)
   163  	}
   164  }
   165  
   166  func (s *snapshotSuite) TestChangeSnapshots400(c *check.C) {
   167  	type table struct{ body, error string }
   168  	tests := []table{
   169  		{
   170  			body:  `"woodchucks`,
   171  			error: "cannot decode request body into snapshot operation:.*",
   172  		}, {
   173  			body:  `{}"woodchucks`,
   174  			error: "extra content found after snapshot operation",
   175  		}, {
   176  			body:  `{}`,
   177  			error: "snapshot operation requires snapshot set ID",
   178  		}, {
   179  			body:  `{"set": 42}`,
   180  			error: "snapshot operation requires action",
   181  		}, {
   182  			body:  `{"set": 42, "action": "carrots"}`,
   183  			error: `unknown snapshot operation "carrots"`,
   184  		}, {
   185  			body:  `{"set": 42, "action": "forget", "users": ["foo"]}`,
   186  			error: `snapshot "forget" operation cannot specify users`,
   187  		},
   188  	}
   189  
   190  	for i, test := range tests {
   191  		comm := check.Commentf("%d:%q", i, test.body)
   192  		req, err := http.NewRequest("POST", "/v2/snapshots", strings.NewReader(test.body))
   193  		c.Assert(err, check.IsNil, comm)
   194  
   195  		rsp := s.req(c, req, nil).(*daemon.Resp)
   196  		c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError, comm)
   197  		c.Check(rsp.Status, check.Equals, 400, comm)
   198  		c.Check(rsp.ErrorResult().Message, check.Matches, test.error, comm)
   199  	}
   200  }
   201  
   202  func (s *snapshotSuite) TestChangeSnapshots404(c *check.C) {
   203  	var done string
   204  	expectedError := errors.New("bzzt")
   205  	defer daemon.MockSnapshotCheck(func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error) {
   206  		done = "check"
   207  		return nil, nil, expectedError
   208  	})()
   209  	defer daemon.MockSnapshotRestore(func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error) {
   210  		done = "restore"
   211  		return nil, nil, expectedError
   212  	})()
   213  	defer daemon.MockSnapshotForget(func(*state.State, uint64, []string) ([]string, *state.TaskSet, error) {
   214  		done = "forget"
   215  		return nil, nil, expectedError
   216  	})()
   217  	for _, expectedError = range []error{client.ErrSnapshotSetNotFound, client.ErrSnapshotSnapsNotFound} {
   218  		for _, action := range []string{"check", "restore", "forget"} {
   219  			done = ""
   220  			comm := check.Commentf("%s/%s", action, expectedError)
   221  			body := fmt.Sprintf(`{"set": 42, "action": "%s"}`, action)
   222  			req, err := http.NewRequest("POST", "/v2/snapshots", strings.NewReader(body))
   223  			c.Assert(err, check.IsNil, comm)
   224  
   225  			rsp := s.req(c, req, nil).(*daemon.Resp)
   226  			c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError, comm)
   227  			c.Check(rsp.Status, check.Equals, 404, comm)
   228  			c.Check(rsp.ErrorResult().Message, check.Matches, expectedError.Error(), comm)
   229  			c.Check(done, check.Equals, action, comm)
   230  		}
   231  	}
   232  }
   233  
   234  func (s *snapshotSuite) TestChangeSnapshots500(c *check.C) {
   235  	var done string
   236  	expectedError := errors.New("bzzt")
   237  	defer daemon.MockSnapshotCheck(func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error) {
   238  		done = "check"
   239  		return nil, nil, expectedError
   240  	})()
   241  	defer daemon.MockSnapshotRestore(func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error) {
   242  		done = "restore"
   243  		return nil, nil, expectedError
   244  	})()
   245  	defer daemon.MockSnapshotForget(func(*state.State, uint64, []string) ([]string, *state.TaskSet, error) {
   246  		done = "forget"
   247  		return nil, nil, expectedError
   248  	})()
   249  	for _, action := range []string{"check", "restore", "forget"} {
   250  		comm := check.Commentf("%s", action)
   251  		body := fmt.Sprintf(`{"set": 42, "action": "%s"}`, action)
   252  		req, err := http.NewRequest("POST", "/v2/snapshots", strings.NewReader(body))
   253  		c.Assert(err, check.IsNil, comm)
   254  
   255  		rsp := s.req(c, req, nil).(*daemon.Resp)
   256  		c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError, comm)
   257  		c.Check(rsp.Status, check.Equals, 500, comm)
   258  		c.Check(rsp.ErrorResult().Message, check.Matches, expectedError.Error(), comm)
   259  		c.Check(done, check.Equals, action, comm)
   260  	}
   261  }
   262  
   263  func (s *snapshotSuite) TestChangeSnapshot(c *check.C) {
   264  	var done string
   265  	defer daemon.MockSnapshotCheck(func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error) {
   266  		done = "check"
   267  		return []string{"foo"}, state.NewTaskSet(), nil
   268  	})()
   269  	defer daemon.MockSnapshotRestore(func(*state.State, uint64, []string, []string) ([]string, *state.TaskSet, error) {
   270  		done = "restore"
   271  		return []string{"foo"}, state.NewTaskSet(), nil
   272  	})()
   273  	defer daemon.MockSnapshotForget(func(*state.State, uint64, []string) ([]string, *state.TaskSet, error) {
   274  		done = "forget"
   275  		return []string{"foo"}, state.NewTaskSet(), nil
   276  	})()
   277  
   278  	st := s.d.Overlord().State()
   279  	st.Lock()
   280  	defer st.Unlock()
   281  	for _, action := range []string{"check", "restore", "forget"} {
   282  		comm := check.Commentf("%s", action)
   283  		body := fmt.Sprintf(`{"set": 42, "action": "%s"}`, action)
   284  		req, err := http.NewRequest("POST", "/v2/snapshots", strings.NewReader(body))
   285  
   286  		c.Assert(err, check.IsNil, comm)
   287  
   288  		st.Unlock()
   289  		rsp := s.req(c, req, nil).(*daemon.Resp)
   290  		st.Lock()
   291  
   292  		c.Check(rsp.Type, check.Equals, daemon.ResponseTypeAsync, comm)
   293  		c.Check(rsp.Status, check.Equals, 202, comm)
   294  		c.Check(done, check.Equals, action, comm)
   295  
   296  		chg := st.Change(rsp.Change)
   297  		c.Assert(chg, check.NotNil)
   298  		c.Assert(chg.Tasks(), check.HasLen, 0)
   299  
   300  		c.Check(chg.Kind(), check.Equals, action+"-snapshot")
   301  		var apiData map[string]interface{}
   302  		err = chg.Get("api-data", &apiData)
   303  		c.Assert(err, check.IsNil)
   304  		c.Check(apiData, check.DeepEquals, map[string]interface{}{
   305  			"snap-names": []interface{}{"foo"},
   306  		})
   307  
   308  	}
   309  }
   310  
   311  func (s *snapshotSuite) TestExportSnapshots(c *check.C) {
   312  	var snapshotExportCalled int
   313  
   314  	defer daemon.MockMuxVars(func(*http.Request) map[string]string {
   315  		return map[string]string{"id": "1"}
   316  	})()
   317  	defer daemon.MockSnapshotExport(func(ctx context.Context, setID uint64) (*snapshotstate.SnapshotExport, error) {
   318  		snapshotExportCalled++
   319  		c.Check(setID, check.Equals, uint64(1))
   320  		return &snapshotstate.SnapshotExport{}, nil
   321  	})()
   322  
   323  	c.Check(daemon.SnapshotExportCmd.Path, check.Equals, "/v2/snapshots/{id}/export")
   324  	req, err := http.NewRequest("GET", "/v2/snapshots/1/export", nil)
   325  	c.Assert(err, check.IsNil)
   326  
   327  	rsp := s.req(c, req, nil)
   328  	c.Check(rsp, check.FitsTypeOf, &daemon.SnapshotExportResponse{})
   329  	c.Check(snapshotExportCalled, check.Equals, 1)
   330  }
   331  
   332  func (s *snapshotSuite) TestExportSnapshotsBadRequestOnNonNumericID(c *check.C) {
   333  	defer daemon.MockMuxVars(func(*http.Request) map[string]string {
   334  		return map[string]string{"id": "xxx"}
   335  	})()
   336  
   337  	c.Check(daemon.SnapshotExportCmd.Path, check.Equals, "/v2/snapshots/{id}/export")
   338  	req, err := http.NewRequest("GET", "/v2/snapshots/xxx/export", nil)
   339  	c.Assert(err, check.IsNil)
   340  
   341  	rsp := s.req(c, req, nil).(*daemon.Resp)
   342  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   343  	c.Check(rsp.Status, check.Equals, 400)
   344  	c.Check(rsp.Result, check.DeepEquals, &daemon.ErrorResult{Message: `'id' must be a positive base 10 number; got "xxx"`})
   345  }
   346  
   347  func (s *snapshotSuite) TestExportSnapshotsBadRequestOnError(c *check.C) {
   348  	var snapshotExportCalled int
   349  
   350  	defer daemon.MockMuxVars(func(*http.Request) map[string]string {
   351  		return map[string]string{"id": "1"}
   352  	})()
   353  	defer daemon.MockSnapshotExport(func(ctx context.Context, setID uint64) (*snapshotstate.SnapshotExport, error) {
   354  		snapshotExportCalled++
   355  		return nil, fmt.Errorf("boom")
   356  	})()
   357  
   358  	c.Check(daemon.SnapshotExportCmd.Path, check.Equals, "/v2/snapshots/{id}/export")
   359  	req, err := http.NewRequest("GET", "/v2/snapshots/1/export", nil)
   360  	c.Assert(err, check.IsNil)
   361  
   362  	rsp := s.req(c, req, nil).(*daemon.Resp)
   363  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeError)
   364  	c.Check(rsp.Status, check.Equals, 400)
   365  	c.Check(rsp.Result, check.DeepEquals, &daemon.ErrorResult{Message: `cannot export 1: boom`})
   366  	c.Check(snapshotExportCalled, check.Equals, 1)
   367  }
   368  
   369  func (s *snapshotSuite) TestImportSnapshot(c *check.C) {
   370  	data := []byte("mocked snapshot export data file")
   371  
   372  	setID := uint64(3)
   373  	snapNames := []string{"baz", "bar", "foo"}
   374  	defer daemon.MockSnapshotImport(func(context.Context, *state.State, io.Reader) (uint64, []string, error) {
   375  		return setID, snapNames, nil
   376  	})()
   377  
   378  	req, err := http.NewRequest("POST", "/v2/snapshots", bytes.NewReader(data))
   379  	req.Header.Add("Content-Length", strconv.Itoa(len(data)))
   380  	c.Assert(err, check.IsNil)
   381  	req.Header.Set("Content-Type", client.SnapshotExportMediaType)
   382  
   383  	rsp := s.req(c, req, nil).(*daemon.Resp)
   384  	c.Check(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   385  	c.Check(rsp.Status, check.Equals, 200)
   386  	c.Check(rsp.Result, check.DeepEquals, map[string]interface{}{"set-id": setID, "snaps": snapNames})
   387  }
   388  
   389  func (s *snapshotSuite) TestImportSnapshotError(c *check.C) {
   390  	defer daemon.MockSnapshotImport(func(context.Context, *state.State, io.Reader) (uint64, []string, error) {
   391  		return uint64(0), nil, errors.New("no")
   392  	})()
   393  
   394  	data := []byte("mocked snapshot export data file")
   395  	req, err := http.NewRequest("POST", "/v2/snapshots", bytes.NewReader(data))
   396  	req.Header.Add("Content-Length", strconv.Itoa(len(data)))
   397  	c.Assert(err, check.IsNil)
   398  	req.Header.Set("Content-Type", client.SnapshotExportMediaType)
   399  
   400  	rsp := s.req(c, req, nil).(*daemon.Resp)
   401  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   402  	c.Check(rsp.Status, check.Equals, 400)
   403  	c.Check(rsp.ErrorResult().Message, check.Equals, "no")
   404  }
   405  
   406  func (s *snapshotSuite) TestImportSnapshotNoContentLengthError(c *check.C) {
   407  	data := []byte("mocked snapshot export data file")
   408  	req, err := http.NewRequest("POST", "/v2/snapshots", bytes.NewReader(data))
   409  	c.Assert(err, check.IsNil)
   410  	req.Header.Set("Content-Type", client.SnapshotExportMediaType)
   411  
   412  	rsp := s.req(c, req, nil).(*daemon.Resp)
   413  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError)
   414  	c.Check(rsp.Status, check.Equals, 400)
   415  	c.Check(rsp.ErrorResult().Message, check.Equals, `cannot parse Content-Length: strconv.ParseInt: parsing "": invalid syntax`)
   416  }
   417  
   418  func (s *snapshotSuite) TestImportSnapshotLimits(c *check.C) {
   419  	var dataRead int
   420  
   421  	defer daemon.MockSnapshotImport(func(ctx context.Context, st *state.State, r io.Reader) (uint64, []string, error) {
   422  		data, err := ioutil.ReadAll(r)
   423  		c.Assert(err, check.IsNil)
   424  		dataRead = len(data)
   425  		return uint64(0), nil, nil
   426  	})()
   427  
   428  	data := []byte("much more data than expected from Content-Length")
   429  	req, err := http.NewRequest("POST", "/v2/snapshots", bytes.NewReader(data))
   430  	// limit to 10 and check that this is really all that is read
   431  	req.Header.Add("Content-Length", "10")
   432  	c.Assert(err, check.IsNil)
   433  	req.Header.Set("Content-Type", client.SnapshotExportMediaType)
   434  
   435  	rsp := s.req(c, req, nil).(*daemon.Resp)
   436  	c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync)
   437  	c.Check(rsp.Status, check.Equals, 200)
   438  	c.Check(dataRead, check.Equals, 10)
   439  }