github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/daemon/api_base_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 "context" 24 "crypto" 25 "fmt" 26 "io/ioutil" 27 "net/http" 28 "os" 29 "path/filepath" 30 "time" 31 32 "github.com/gorilla/mux" 33 "golang.org/x/crypto/sha3" 34 "gopkg.in/check.v1" 35 "gopkg.in/tomb.v2" 36 37 "github.com/snapcore/snapd/asserts" 38 "github.com/snapcore/snapd/asserts/assertstest" 39 "github.com/snapcore/snapd/asserts/sysdb" 40 "github.com/snapcore/snapd/daemon" 41 "github.com/snapcore/snapd/dirs" 42 "github.com/snapcore/snapd/osutil" 43 "github.com/snapcore/snapd/overlord" 44 "github.com/snapcore/snapd/overlord/assertstate" 45 "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" 46 "github.com/snapcore/snapd/overlord/auth" 47 "github.com/snapcore/snapd/overlord/devicestate" 48 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 49 "github.com/snapcore/snapd/overlord/ifacestate" 50 "github.com/snapcore/snapd/overlord/snapstate" 51 "github.com/snapcore/snapd/overlord/state" 52 "github.com/snapcore/snapd/sandbox" 53 "github.com/snapcore/snapd/snap" 54 "github.com/snapcore/snapd/snap/snaptest" 55 "github.com/snapcore/snapd/store" 56 "github.com/snapcore/snapd/store/storetest" 57 "github.com/snapcore/snapd/systemd" 58 "github.com/snapcore/snapd/testutil" 59 ) 60 61 // TODO: as we split api_test.go and move more tests to live in daemon_test 62 // instead of daemon, split out functionality from APIBaseSuite 63 // to only the relevant suite when possible 64 65 type apiBaseSuite struct { 66 testutil.BaseTest 67 68 storetest.Store 69 70 rsnaps []*snap.Info 71 err error 72 vars map[string]string 73 storeSearch store.Search 74 suggestedCurrency string 75 d *daemon.Daemon 76 user *auth.UserState 77 ctx context.Context 78 currentSnaps []*store.CurrentSnap 79 actions []*store.SnapAction 80 81 restoreRelease func() 82 83 StoreSigning *assertstest.StoreStack 84 Brands *assertstest.SigningAccounts 85 86 systemctlRestorer func() 87 SysctlBufs [][]byte 88 89 connectivityResult map[string]bool 90 91 restoreSanitize func() 92 restoreMuxVars func() 93 } 94 95 func (s *apiBaseSuite) pokeStateLock() { 96 // the store should be called without the state lock held. Try 97 // to acquire it. 98 st := s.d.Overlord().State() 99 st.Lock() 100 st.Unlock() 101 } 102 103 func (s *apiBaseSuite) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { 104 s.pokeStateLock() 105 s.user = user 106 s.ctx = ctx 107 if len(s.rsnaps) > 0 { 108 return s.rsnaps[0], s.err 109 } 110 return nil, s.err 111 } 112 113 func (s *apiBaseSuite) Find(ctx context.Context, search *store.Search, user *auth.UserState) ([]*snap.Info, error) { 114 s.pokeStateLock() 115 116 s.storeSearch = *search 117 s.user = user 118 s.ctx = ctx 119 120 return s.rsnaps, s.err 121 } 122 123 func (s *apiBaseSuite) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, assertQuery store.AssertionQuery, user *auth.UserState, opts *store.RefreshOptions) ([]store.SnapActionResult, []store.AssertionResult, error) { 124 s.pokeStateLock() 125 if assertQuery != nil { 126 toResolve, toResolveSeq, err := assertQuery.ToResolve() 127 if err != nil { 128 return nil, nil, err 129 } 130 if len(toResolve) != 0 || len(toResolveSeq) != 0 { 131 panic("no assertion query support") 132 } 133 } 134 135 if ctx == nil { 136 panic("context required") 137 } 138 s.currentSnaps = currentSnaps 139 s.actions = actions 140 s.user = user 141 142 sars := make([]store.SnapActionResult, len(s.rsnaps)) 143 for i, rsnap := range s.rsnaps { 144 sars[i] = store.SnapActionResult{Info: rsnap} 145 } 146 return sars, nil, s.err 147 } 148 149 func (s *apiBaseSuite) SuggestedCurrency() string { 150 s.pokeStateLock() 151 152 return s.suggestedCurrency 153 } 154 155 func (s *apiBaseSuite) ConnectivityCheck() (map[string]bool, error) { 156 s.pokeStateLock() 157 158 return s.connectivityResult, s.err 159 } 160 161 func (s *apiBaseSuite) muxVars(*http.Request) map[string]string { 162 return s.vars 163 } 164 165 func (s *apiBaseSuite) SetUpSuite(c *check.C) { 166 s.restoreMuxVars = daemon.MockMuxVars(s.muxVars) 167 s.restoreRelease = sandbox.MockForceDevMode(false) 168 s.systemctlRestorer = systemd.MockSystemctl(s.systemctl) 169 s.restoreSanitize = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 170 } 171 172 func (s *apiBaseSuite) TearDownSuite(c *check.C) { 173 s.restoreMuxVars() 174 s.restoreRelease() 175 s.systemctlRestorer() 176 s.restoreSanitize() 177 } 178 179 func (s *apiBaseSuite) systemctl(args ...string) (buf []byte, err error) { 180 if len(s.SysctlBufs) > 0 { 181 buf, s.SysctlBufs = s.SysctlBufs[0], s.SysctlBufs[1:] 182 } 183 return buf, err 184 } 185 186 var ( 187 brandPrivKey, _ = assertstest.GenerateKey(752) 188 ) 189 190 func (s *apiBaseSuite) SetUpTest(c *check.C) { 191 s.BaseTest.SetUpTest(c) 192 193 ctlcmds := testutil.MockCommand(c, "systemctl", "").Also("journalctl", "") 194 s.AddCleanup(ctlcmds.Restore) 195 196 s.SysctlBufs = nil 197 198 dirs.SetRootDir(c.MkDir()) 199 err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) 200 restore := osutil.MockMountInfo("") 201 s.AddCleanup(restore) 202 203 c.Assert(err, check.IsNil) 204 c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil) 205 c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil) 206 207 s.rsnaps = nil 208 s.suggestedCurrency = "" 209 s.storeSearch = store.Search{} 210 s.err = nil 211 s.vars = nil 212 s.user = nil 213 s.d = nil 214 s.currentSnaps = nil 215 s.actions = nil 216 // Disable real security backends for all API tests 217 s.AddCleanup(ifacestate.MockSecurityBackends(nil)) 218 219 s.StoreSigning = assertstest.NewStoreStack("can0nical", nil) 220 s.AddCleanup(sysdb.InjectTrusted(s.StoreSigning.Trusted)) 221 222 s.Brands = assertstest.NewSigningAccounts(s.StoreSigning) 223 s.Brands.Register("my-brand", brandPrivKey, nil) 224 } 225 226 func (s *apiBaseSuite) TearDownTest(c *check.C) { 227 s.d = nil 228 s.ctx = nil 229 230 dirs.SetRootDir("") 231 s.BaseTest.TearDownTest(c) 232 } 233 234 func (s *apiBaseSuite) mockModel(c *check.C, st *state.State, model *asserts.Model) { 235 // realistic model setup 236 if model == nil { 237 model = s.Brands.Model("can0nical", "pc", map[string]interface{}{ 238 "architecture": "amd64", 239 "gadget": "gadget", 240 "kernel": "kernel", 241 }) 242 } 243 244 snapstate.DeviceCtx = devicestate.DeviceCtx 245 246 assertstatetest.AddMany(st, model) 247 248 devicestatetest.SetDevice(st, &auth.DeviceState{ 249 Brand: model.BrandID(), 250 Model: model.Model(), 251 Serial: "serialserial", 252 }) 253 } 254 255 func (s *apiBaseSuite) daemonWithStore(c *check.C, sto snapstate.StoreService) *daemon.Daemon { 256 if s.d != nil { 257 panic("called daemon*() twice") 258 } 259 d, err := daemon.NewAndAddRoutes() 260 c.Assert(err, check.IsNil) 261 262 st := d.Overlord().State() 263 // mark as already seeded 264 st.Lock() 265 st.Set("seeded", true) 266 st.Unlock() 267 c.Assert(d.Overlord().StartUp(), check.IsNil) 268 269 st.Lock() 270 defer st.Unlock() 271 snapstate.ReplaceStore(st, sto) 272 // registered 273 s.mockModel(c, st, nil) 274 275 // don't actually try to talk to the store on snapstate.Ensure 276 // needs doing after the call to devicestate.Manager (which 277 // happens in daemon.New via overlord.New) 278 snapstate.CanAutoRefresh = nil 279 280 s.d = d 281 return d 282 } 283 284 func (s *apiBaseSuite) resetDaemon() { 285 s.d = nil 286 } 287 288 func (s *apiBaseSuite) daemon(c *check.C) *daemon.Daemon { 289 return s.daemonWithStore(c, s) 290 } 291 292 func (s *apiBaseSuite) daemonWithOverlordMock(c *check.C) *daemon.Daemon { 293 if s.d != nil { 294 panic("called daemon*() twice") 295 } 296 297 o := overlord.Mock() 298 s.d = daemon.NewWithOverlord(o) 299 return s.d 300 } 301 302 func (s *apiBaseSuite) daemonWithOverlordMockAndStore(c *check.C) *daemon.Daemon { 303 if s.d != nil { 304 panic("called daemon*() twice") 305 } 306 307 o := overlord.Mock() 308 d := daemon.NewWithOverlord(o) 309 310 st := d.Overlord().State() 311 // adds an assertion db 312 assertstate.Manager(st, o.TaskRunner()) 313 st.Lock() 314 defer st.Unlock() 315 snapstate.ReplaceStore(st, s) 316 317 s.d = d 318 return d 319 } 320 321 type fakeSnapManager struct{} 322 323 func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapManager { 324 runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error { 325 return nil 326 }, nil) 327 runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error { 328 return fmt.Errorf("fake-install-snap-error errored") 329 }, nil) 330 331 return &fakeSnapManager{} 332 } 333 334 func (m *fakeSnapManager) Ensure() error { 335 return nil 336 } 337 338 // sanity 339 var _ overlord.StateManager = (*fakeSnapManager)(nil) 340 341 func (s *apiBaseSuite) daemonWithFakeSnapManager(c *check.C) *daemon.Daemon { 342 d := s.daemonWithOverlordMockAndStore(c) 343 st := d.Overlord().State() 344 runner := d.Overlord().TaskRunner() 345 d.Overlord().AddManager(newFakeSnapManager(st, runner)) 346 d.Overlord().AddManager(runner) 347 c.Assert(d.Overlord().StartUp(), check.IsNil) 348 return d 349 } 350 351 func (s *apiBaseSuite) waitTrivialChange(c *check.C, chg *state.Change) { 352 err := s.d.Overlord().Settle(5 * time.Second) 353 c.Assert(err, check.IsNil) 354 c.Assert(chg.IsReady(), check.Equals, true) 355 } 356 357 func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string { 358 df := filepath.Join(dirs.SnapDesktopFilesDir, name) 359 err := os.MkdirAll(filepath.Dir(df), 0755) 360 c.Assert(err, check.IsNil) 361 err = ioutil.WriteFile(df, []byte(content), 0644) 362 c.Assert(err, check.IsNil) 363 return df 364 } 365 366 func (s *apiBaseSuite) mockSnap(c *check.C, yamlText string) *snap.Info { 367 if s.d == nil { 368 panic("call s.daemon(c) etc in your test first") 369 } 370 371 snapInfo := snaptest.MockSnap(c, yamlText, &snap.SideInfo{Revision: snap.R(1)}) 372 373 st := s.d.Overlord().State() 374 375 st.Lock() 376 defer st.Unlock() 377 378 // Put a side info into the state 379 snapstate.Set(st, snapInfo.InstanceName(), &snapstate.SnapState{ 380 Active: true, 381 Sequence: []*snap.SideInfo{ 382 { 383 RealName: snapInfo.SnapName(), 384 Revision: snapInfo.Revision, 385 SnapID: "ididid", 386 }, 387 }, 388 Current: snapInfo.Revision, 389 SnapType: string(snapInfo.Type()), 390 }) 391 392 // Put the snap into the interface repository 393 repo := s.d.Overlord().InterfaceManager().Repository() 394 err := repo.AddSnap(snapInfo) 395 c.Assert(err, check.IsNil) 396 return snapInfo 397 } 398 399 func (s *apiBaseSuite) mkInstalledInState(c *check.C, d *daemon.Daemon, instanceName, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { 400 snapName, instanceKey := snap.SplitInstanceName(instanceName) 401 402 if revision.Local() && developer != "" { 403 panic("not supported") 404 } 405 406 var snapID string 407 if revision.Store() { 408 snapID = snapName + "-id" 409 } 410 // Collect arguments into a snap.SideInfo structure 411 sideInfo := &snap.SideInfo{ 412 SnapID: snapID, 413 RealName: snapName, 414 Revision: revision, 415 Channel: "stable", 416 } 417 418 // Collect other arguments into a yaml string 419 yamlText := fmt.Sprintf(` 420 name: %s 421 version: %s 422 %s`, snapName, version, extraYaml) 423 424 // Mock the snap on disk 425 snapInfo := snaptest.MockSnapInstance(c, instanceName, yamlText, sideInfo) 426 if active { 427 dir, rev := filepath.Split(snapInfo.MountDir()) 428 c.Assert(os.Symlink(rev, dir+"current"), check.IsNil) 429 } 430 c.Assert(snapInfo.InstanceName(), check.Equals, instanceName) 431 432 c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil) 433 metadir := filepath.Join(snapInfo.MountDir(), "meta") 434 guidir := filepath.Join(metadir, "gui") 435 c.Assert(os.MkdirAll(guidir, 0755), check.IsNil) 436 c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil) 437 438 if d == nil { 439 return snapInfo 440 } 441 st := d.Overlord().State() 442 st.Lock() 443 defer st.Unlock() 444 445 var snapst snapstate.SnapState 446 snapstate.Get(st, instanceName, &snapst) 447 snapst.Active = active 448 snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo) 449 snapst.Current = snapInfo.SideInfo.Revision 450 snapst.TrackingChannel = "stable" 451 snapst.InstanceKey = instanceKey 452 453 snapstate.Set(st, instanceName, &snapst) 454 455 if developer == "" { 456 return snapInfo 457 } 458 459 devAcct := assertstest.NewAccount(s.StoreSigning, developer, map[string]interface{}{ 460 "account-id": developer + "-id", 461 }, "") 462 463 snapInfo.Publisher = snap.StoreAccount{ 464 ID: devAcct.AccountID(), 465 Username: devAcct.Username(), 466 DisplayName: devAcct.DisplayName(), 467 Validation: devAcct.Validation(), 468 } 469 470 snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ 471 "series": "16", 472 "snap-id": snapID, 473 "snap-name": snapName, 474 "publisher-id": devAcct.AccountID(), 475 "timestamp": time.Now().Format(time.RFC3339), 476 }, nil, "") 477 c.Assert(err, check.IsNil) 478 479 content, err := ioutil.ReadFile(snapInfo.MountFile()) 480 c.Assert(err, check.IsNil) 481 h := sha3.Sum384(content) 482 dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:]) 483 c.Assert(err, check.IsNil) 484 snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ 485 "snap-sha3-384": string(dgst), 486 "snap-size": "999", 487 "snap-id": snapID, 488 "snap-revision": fmt.Sprintf("%s", revision), 489 "developer-id": devAcct.AccountID(), 490 "timestamp": time.Now().Format(time.RFC3339), 491 }, nil, "") 492 c.Assert(err, check.IsNil) 493 494 assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), devAcct, snapDecl, snapRev) 495 496 return snapInfo 497 } 498 499 func handlerCommand(c *check.C, d *daemon.Daemon, req *http.Request) (cmd *daemon.Command, vars map[string]string) { 500 m := &mux.RouteMatch{} 501 if !d.RouterMatch(req, m) { 502 c.Fatalf("no command for URL %q", req.URL) 503 } 504 cmd, ok := m.Handler.(*daemon.Command) 505 if !ok { 506 c.Fatalf("no command for URL %q", req.URL) 507 } 508 return cmd, m.Vars 509 } 510 511 func (s *apiBaseSuite) checkGetOnly(c *check.C, req *http.Request) { 512 if s.d == nil { 513 panic("call s.daemon(c) etc in your test first") 514 } 515 516 cmd, _ := handlerCommand(c, s.d, req) 517 c.Check(cmd.POST, check.IsNil) 518 c.Check(cmd.PUT, check.IsNil) 519 c.Check(cmd.GET, check.NotNil) 520 } 521 522 func (s *apiBaseSuite) req(c *check.C, req *http.Request, u *auth.UserState) daemon.Response { 523 if s.d == nil { 524 panic("call s.daemon(c) etc in your test first") 525 } 526 527 cmd, vars := handlerCommand(c, s.d, req) 528 s.vars = vars 529 var f daemon.ResponseFunc 530 switch req.Method { 531 case "GET": 532 f = cmd.GET 533 case "POST": 534 f = cmd.POST 535 case "PUT": 536 f = cmd.PUT 537 default: 538 c.Fatalf("unsupported HTTP method %q", req.Method) 539 } 540 if f == nil { 541 c.Fatalf("no support for %q for %q", req.Method, req.URL) 542 } 543 return f(cmd, req, u) 544 } 545 546 func (s *apiBaseSuite) jsonReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp { 547 rsp, ok := s.req(c, req, u).(*daemon.Resp) 548 c.Assert(ok, check.Equals, true, check.Commentf("expected structured response")) 549 return rsp 550 } 551 552 func (s *apiBaseSuite) syncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp { 553 rsp := s.jsonReq(c, req, u) 554 c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync, check.Commentf("expected sync resp: %#v", rsp)) 555 return rsp 556 } 557 558 func (s *apiBaseSuite) asyncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp { 559 rsp := s.jsonReq(c, req, u) 560 c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeAsync, check.Commentf("expected async resp: %#v", rsp)) 561 return rsp 562 } 563 564 func (s *apiBaseSuite) errorReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.Resp { 565 rsp := s.jsonReq(c, req, u) 566 c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError, check.Commentf("expected error resp: %#v", rsp)) 567 return rsp 568 } 569 570 func (s *apiBaseSuite) serveHTTP(c *check.C, w http.ResponseWriter, req *http.Request) { 571 if s.d == nil { 572 panic("call s.daemon(c) etc in your test first") 573 } 574 575 cmd, vars := handlerCommand(c, s.d, req) 576 s.vars = vars 577 578 cmd.ServeHTTP(w, req) 579 } 580 581 func (s *apiBaseSuite) simulateConflict(name string) { 582 if s.d == nil { 583 panic("call s.daemon(c) etc in your test first") 584 } 585 586 o := s.d.Overlord() 587 st := o.State() 588 st.Lock() 589 defer st.Unlock() 590 t := st.NewTask("link-snap", "...") 591 snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{ 592 RealName: name, 593 }} 594 t.Set("snap-setup", snapsup) 595 chg := st.NewChange("manip", "...") 596 chg.AddTask(t) 597 }