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 }