github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/daemon/api_base_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2021 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 authUser *auth.UserState 95 96 expectedReadAccess daemon.AccessChecker 97 expectedWriteAccess daemon.AccessChecker 98 } 99 100 func (s *apiBaseSuite) pokeStateLock() { 101 // the store should be called without the state lock held. Try 102 // to acquire it. 103 st := s.d.Overlord().State() 104 st.Lock() 105 st.Unlock() 106 } 107 108 func (s *apiBaseSuite) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { 109 s.pokeStateLock() 110 s.user = user 111 s.ctx = ctx 112 if len(s.rsnaps) > 0 { 113 return s.rsnaps[0], s.err 114 } 115 return nil, s.err 116 } 117 118 func (s *apiBaseSuite) Find(ctx context.Context, search *store.Search, user *auth.UserState) ([]*snap.Info, error) { 119 s.pokeStateLock() 120 121 s.storeSearch = *search 122 s.user = user 123 s.ctx = ctx 124 125 return s.rsnaps, s.err 126 } 127 128 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) { 129 s.pokeStateLock() 130 if assertQuery != nil { 131 toResolve, toResolveSeq, err := assertQuery.ToResolve() 132 if err != nil { 133 return nil, nil, err 134 } 135 if len(toResolve) != 0 || len(toResolveSeq) != 0 { 136 panic("no assertion query support") 137 } 138 } 139 140 if ctx == nil { 141 panic("context required") 142 } 143 s.currentSnaps = currentSnaps 144 s.actions = actions 145 s.user = user 146 147 sars := make([]store.SnapActionResult, len(s.rsnaps)) 148 for i, rsnap := range s.rsnaps { 149 sars[i] = store.SnapActionResult{Info: rsnap} 150 } 151 return sars, nil, s.err 152 } 153 154 func (s *apiBaseSuite) SuggestedCurrency() string { 155 s.pokeStateLock() 156 157 return s.suggestedCurrency 158 } 159 160 func (s *apiBaseSuite) ConnectivityCheck() (map[string]bool, error) { 161 s.pokeStateLock() 162 163 return s.connectivityResult, s.err 164 } 165 166 func (s *apiBaseSuite) muxVars(*http.Request) map[string]string { 167 return s.vars 168 } 169 170 func (s *apiBaseSuite) SetUpSuite(c *check.C) { 171 s.restoreMuxVars = daemon.MockMuxVars(s.muxVars) 172 s.restoreRelease = sandbox.MockForceDevMode(false) 173 s.systemctlRestorer = systemd.MockSystemctl(s.systemctl) 174 s.restoreSanitize = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 175 } 176 177 func (s *apiBaseSuite) TearDownSuite(c *check.C) { 178 s.restoreMuxVars() 179 s.restoreRelease() 180 s.systemctlRestorer() 181 s.restoreSanitize() 182 } 183 184 func (s *apiBaseSuite) systemctl(args ...string) (buf []byte, err error) { 185 if len(s.SysctlBufs) > 0 { 186 buf, s.SysctlBufs = s.SysctlBufs[0], s.SysctlBufs[1:] 187 } 188 return buf, err 189 } 190 191 var ( 192 brandPrivKey, _ = assertstest.GenerateKey(752) 193 ) 194 195 func (s *apiBaseSuite) SetUpTest(c *check.C) { 196 s.BaseTest.SetUpTest(c) 197 198 ctlcmds := testutil.MockCommand(c, "systemctl", "").Also("journalctl", "") 199 s.AddCleanup(ctlcmds.Restore) 200 201 s.SysctlBufs = nil 202 203 dirs.SetRootDir(c.MkDir()) 204 err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) 205 restore := osutil.MockMountInfo("") 206 s.AddCleanup(restore) 207 208 c.Assert(err, check.IsNil) 209 c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil) 210 c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil) 211 212 s.rsnaps = nil 213 s.suggestedCurrency = "" 214 s.storeSearch = store.Search{} 215 s.err = nil 216 s.vars = nil 217 s.user = nil 218 s.d = nil 219 s.currentSnaps = nil 220 s.actions = nil 221 s.authUser = nil 222 223 // TODO: consider making the default ReadAccess expectation 224 // authenticatedAccess, but that would need even more test changes 225 s.expectedReadAccess = daemon.OpenAccess{} 226 s.expectedWriteAccess = daemon.AuthenticatedAccess{} 227 228 // Disable real security backends for all API tests 229 s.AddCleanup(ifacestate.MockSecurityBackends(nil)) 230 231 s.StoreSigning = assertstest.NewStoreStack("can0nical", nil) 232 s.AddCleanup(sysdb.InjectTrusted(s.StoreSigning.Trusted)) 233 234 s.Brands = assertstest.NewSigningAccounts(s.StoreSigning) 235 s.Brands.Register("my-brand", brandPrivKey, nil) 236 } 237 238 func (s *apiBaseSuite) TearDownTest(c *check.C) { 239 s.d = nil 240 s.ctx = nil 241 242 dirs.SetRootDir("") 243 s.BaseTest.TearDownTest(c) 244 } 245 246 func (s *apiBaseSuite) mockModel(c *check.C, st *state.State, model *asserts.Model) { 247 // realistic model setup 248 if model == nil { 249 model = s.Brands.Model("can0nical", "pc", map[string]interface{}{ 250 "architecture": "amd64", 251 "gadget": "gadget", 252 "kernel": "kernel", 253 }) 254 } 255 256 snapstate.DeviceCtx = devicestate.DeviceCtx 257 258 assertstatetest.AddMany(st, model) 259 260 devicestatetest.SetDevice(st, &auth.DeviceState{ 261 Brand: model.BrandID(), 262 Model: model.Model(), 263 Serial: "serialserial", 264 }) 265 } 266 267 func (s *apiBaseSuite) daemonWithStore(c *check.C, sto snapstate.StoreService) *daemon.Daemon { 268 if s.d != nil { 269 panic("called daemon*() twice") 270 } 271 d, err := daemon.NewAndAddRoutes() 272 c.Assert(err, check.IsNil) 273 274 st := d.Overlord().State() 275 // mark as already seeded 276 st.Lock() 277 st.Set("seeded", true) 278 st.Unlock() 279 c.Assert(d.Overlord().StartUp(), check.IsNil) 280 281 st.Lock() 282 defer st.Unlock() 283 snapstate.ReplaceStore(st, sto) 284 // registered 285 s.mockModel(c, st, nil) 286 287 // don't actually try to talk to the store on snapstate.Ensure 288 // needs doing after the call to devicestate.Manager (which 289 // happens in daemon.New via overlord.New) 290 snapstate.CanAutoRefresh = nil 291 292 s.d = d 293 return d 294 } 295 296 func (s *apiBaseSuite) resetDaemon() { 297 s.d = nil 298 } 299 300 func (s *apiBaseSuite) daemon(c *check.C) *daemon.Daemon { 301 return s.daemonWithStore(c, s) 302 } 303 304 func (s *apiBaseSuite) daemonWithOverlordMock(c *check.C) *daemon.Daemon { 305 if s.d != nil { 306 panic("called daemon*() twice") 307 } 308 309 o := overlord.Mock() 310 s.d = daemon.NewWithOverlord(o) 311 return s.d 312 } 313 314 func (s *apiBaseSuite) daemonWithOverlordMockAndStore(c *check.C) *daemon.Daemon { 315 if s.d != nil { 316 panic("called daemon*() twice") 317 } 318 319 o := overlord.Mock() 320 d := daemon.NewWithOverlord(o) 321 322 st := d.Overlord().State() 323 // adds an assertion db 324 assertstate.Manager(st, o.TaskRunner()) 325 st.Lock() 326 defer st.Unlock() 327 snapstate.ReplaceStore(st, s) 328 329 s.d = d 330 return d 331 } 332 333 // asUserAuth fakes authorization into the request as for root 334 func (s *apiBaseSuite) asRootAuth(req *http.Request) { 335 req.RemoteAddr = fmt.Sprintf("pid=100;uid=0;socket=%s;", dirs.SnapdSocket) 336 } 337 338 // asUserAuth adds authorization to the request as for a logged in user 339 func (s *apiBaseSuite) asUserAuth(c *check.C, req *http.Request) { 340 if s.d == nil { 341 panic("call s.daemon(c) etc in your test first") 342 } 343 if s.authUser == nil { 344 st := s.d.Overlord().State() 345 st.Lock() 346 u, err := auth.NewUser(st, "username", "email@test.com", "macaroon", []string{"discharge"}) 347 st.Unlock() 348 c.Assert(err, check.IsNil) 349 s.authUser = u 350 } 351 req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, s.authUser.Macaroon)) 352 req.RemoteAddr = fmt.Sprintf("pid=100;uid=1000;socket=%s;", dirs.SnapdSocket) 353 } 354 355 type fakeSnapManager struct{} 356 357 func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapManager { 358 runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error { 359 return nil 360 }, nil) 361 runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error { 362 return fmt.Errorf("fake-install-snap-error errored") 363 }, nil) 364 365 return &fakeSnapManager{} 366 } 367 368 func (m *fakeSnapManager) Ensure() error { 369 return nil 370 } 371 372 // sanity 373 var _ overlord.StateManager = (*fakeSnapManager)(nil) 374 375 func (s *apiBaseSuite) daemonWithFakeSnapManager(c *check.C) *daemon.Daemon { 376 d := s.daemonWithOverlordMockAndStore(c) 377 st := d.Overlord().State() 378 runner := d.Overlord().TaskRunner() 379 d.Overlord().AddManager(newFakeSnapManager(st, runner)) 380 d.Overlord().AddManager(runner) 381 c.Assert(d.Overlord().StartUp(), check.IsNil) 382 return d 383 } 384 385 func (s *apiBaseSuite) waitTrivialChange(c *check.C, chg *state.Change) { 386 err := s.d.Overlord().Settle(5 * time.Second) 387 c.Assert(err, check.IsNil) 388 c.Assert(chg.IsReady(), check.Equals, true) 389 } 390 391 func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string { 392 df := filepath.Join(dirs.SnapDesktopFilesDir, name) 393 err := os.MkdirAll(filepath.Dir(df), 0755) 394 c.Assert(err, check.IsNil) 395 err = ioutil.WriteFile(df, []byte(content), 0644) 396 c.Assert(err, check.IsNil) 397 return df 398 } 399 400 func (s *apiBaseSuite) mockSnap(c *check.C, yamlText string) *snap.Info { 401 if s.d == nil { 402 panic("call s.daemon(c) etc in your test first") 403 } 404 405 snapInfo := snaptest.MockSnap(c, yamlText, &snap.SideInfo{Revision: snap.R(1)}) 406 407 st := s.d.Overlord().State() 408 409 st.Lock() 410 defer st.Unlock() 411 412 // Put a side info into the state 413 snapstate.Set(st, snapInfo.InstanceName(), &snapstate.SnapState{ 414 Active: true, 415 Sequence: []*snap.SideInfo{ 416 { 417 RealName: snapInfo.SnapName(), 418 Revision: snapInfo.Revision, 419 SnapID: "ididid", 420 }, 421 }, 422 Current: snapInfo.Revision, 423 SnapType: string(snapInfo.Type()), 424 }) 425 426 // Put the snap into the interface repository 427 repo := s.d.Overlord().InterfaceManager().Repository() 428 err := repo.AddSnap(snapInfo) 429 c.Assert(err, check.IsNil) 430 return snapInfo 431 } 432 433 func (s *apiBaseSuite) mkInstalledInState(c *check.C, d *daemon.Daemon, instanceName, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { 434 snapName, instanceKey := snap.SplitInstanceName(instanceName) 435 436 if revision.Local() && developer != "" { 437 panic("not supported") 438 } 439 440 var snapID string 441 if revision.Store() { 442 snapID = snapName + "-id" 443 } 444 // Collect arguments into a snap.SideInfo structure 445 sideInfo := &snap.SideInfo{ 446 SnapID: snapID, 447 RealName: snapName, 448 Revision: revision, 449 Channel: "stable", 450 } 451 452 // Collect other arguments into a yaml string 453 yamlText := fmt.Sprintf(` 454 name: %s 455 version: %s 456 %s`, snapName, version, extraYaml) 457 458 // Mock the snap on disk 459 snapInfo := snaptest.MockSnapInstance(c, instanceName, yamlText, sideInfo) 460 if active { 461 dir, rev := filepath.Split(snapInfo.MountDir()) 462 c.Assert(os.Symlink(rev, dir+"current"), check.IsNil) 463 } 464 c.Assert(snapInfo.InstanceName(), check.Equals, instanceName) 465 466 c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil) 467 metadir := filepath.Join(snapInfo.MountDir(), "meta") 468 guidir := filepath.Join(metadir, "gui") 469 c.Assert(os.MkdirAll(guidir, 0755), check.IsNil) 470 c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil) 471 472 if d == nil { 473 return snapInfo 474 } 475 st := d.Overlord().State() 476 st.Lock() 477 defer st.Unlock() 478 479 var snapst snapstate.SnapState 480 snapstate.Get(st, instanceName, &snapst) 481 snapst.Active = active 482 snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo) 483 snapst.Current = snapInfo.SideInfo.Revision 484 snapst.TrackingChannel = "stable" 485 snapst.InstanceKey = instanceKey 486 487 snapstate.Set(st, instanceName, &snapst) 488 489 if developer == "" { 490 return snapInfo 491 } 492 493 devAcct := assertstest.NewAccount(s.StoreSigning, developer, map[string]interface{}{ 494 "account-id": developer + "-id", 495 }, "") 496 497 snapInfo.Publisher = snap.StoreAccount{ 498 ID: devAcct.AccountID(), 499 Username: devAcct.Username(), 500 DisplayName: devAcct.DisplayName(), 501 Validation: devAcct.Validation(), 502 } 503 504 snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ 505 "series": "16", 506 "snap-id": snapID, 507 "snap-name": snapName, 508 "publisher-id": devAcct.AccountID(), 509 "timestamp": time.Now().Format(time.RFC3339), 510 }, nil, "") 511 c.Assert(err, check.IsNil) 512 513 content, err := ioutil.ReadFile(snapInfo.MountFile()) 514 c.Assert(err, check.IsNil) 515 h := sha3.Sum384(content) 516 dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:]) 517 c.Assert(err, check.IsNil) 518 snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ 519 "snap-sha3-384": string(dgst), 520 "snap-size": "999", 521 "snap-id": snapID, 522 "snap-revision": revision.String(), // this must be a string 523 "developer-id": devAcct.AccountID(), 524 "timestamp": time.Now().Format(time.RFC3339), 525 }, nil, "") 526 c.Assert(err, check.IsNil) 527 528 assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), devAcct, snapDecl, snapRev) 529 530 return snapInfo 531 } 532 533 func handlerCommand(c *check.C, d *daemon.Daemon, req *http.Request) (cmd *daemon.Command, vars map[string]string) { 534 m := &mux.RouteMatch{} 535 if !d.RouterMatch(req, m) { 536 c.Fatalf("no command for URL %q", req.URL) 537 } 538 cmd, ok := m.Handler.(*daemon.Command) 539 if !ok { 540 c.Fatalf("no command for URL %q", req.URL) 541 } 542 return cmd, m.Vars 543 } 544 545 func (s *apiBaseSuite) checkGetOnly(c *check.C, req *http.Request) { 546 if s.d == nil { 547 panic("call s.daemon(c) etc in your test first") 548 } 549 550 cmd, _ := handlerCommand(c, s.d, req) 551 c.Check(cmd.POST, check.IsNil) 552 c.Check(cmd.PUT, check.IsNil) 553 c.Check(cmd.GET, check.NotNil) 554 } 555 556 func (s *apiBaseSuite) expectOpenAccess() { 557 s.expectedReadAccess = daemon.OpenAccess{} 558 } 559 560 func (s *apiBaseSuite) expectRootAccess() { 561 s.expectedReadAccess = daemon.RootAccess{} 562 s.expectedWriteAccess = daemon.RootAccess{} 563 } 564 565 func (s *apiBaseSuite) expectAuthenticatedAccess() { 566 s.expectedReadAccess = daemon.AuthenticatedAccess{} 567 s.expectedWriteAccess = daemon.AuthenticatedAccess{} 568 } 569 570 func (s *apiBaseSuite) expectReadAccess(a daemon.AccessChecker) { 571 s.expectedReadAccess = a 572 } 573 574 func (s *apiBaseSuite) expectWriteAccess(a daemon.AccessChecker) { 575 s.expectedWriteAccess = a 576 } 577 578 func (s *apiBaseSuite) req(c *check.C, req *http.Request, u *auth.UserState) daemon.Response { 579 if s.d == nil { 580 panic("call s.daemon(c) etc in your test first") 581 } 582 583 cmd, vars := handlerCommand(c, s.d, req) 584 s.vars = vars 585 var f daemon.ResponseFunc 586 var acc, expAcc daemon.AccessChecker 587 var whichAcc string 588 switch req.Method { 589 case "GET": 590 f = cmd.GET 591 acc = cmd.ReadAccess 592 expAcc = s.expectedReadAccess 593 whichAcc = "ReadAccess" 594 case "POST": 595 f = cmd.POST 596 acc = cmd.WriteAccess 597 expAcc = s.expectedWriteAccess 598 whichAcc = "WriteAccess" 599 case "PUT": 600 f = cmd.PUT 601 acc = cmd.WriteAccess 602 expAcc = s.expectedWriteAccess 603 whichAcc = "WriteAccess" 604 default: 605 c.Fatalf("unsupported HTTP method %q", req.Method) 606 } 607 if f == nil { 608 c.Fatalf("no support for %q for %q", req.Method, req.URL) 609 } 610 c.Check(acc, check.DeepEquals, expAcc, check.Commentf("expected %s check mismatch, use the apiBaseSuite.expect*Access methods to match the appropriate access check for the API under test", whichAcc)) 611 return f(cmd, req, u) 612 } 613 614 func (s *apiBaseSuite) jsonReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.RespJSON { 615 rsp, ok := s.req(c, req, u).(daemon.StructuredResponse) 616 c.Assert(ok, check.Equals, true, check.Commentf("expected structured response")) 617 return rsp.JSON() 618 } 619 620 func (s *apiBaseSuite) syncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.RespJSON { 621 rsp := s.jsonReq(c, req, u) 622 c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeSync, check.Commentf("expected sync resp: %#v, result: %+v", rsp, rsp.Result)) 623 return rsp 624 } 625 626 func (s *apiBaseSuite) asyncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.RespJSON { 627 rsp := s.jsonReq(c, req, u) 628 c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeAsync, check.Commentf("expected async resp: %#v", rsp)) 629 return rsp 630 } 631 632 func (s *apiBaseSuite) errorReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.APIError { 633 rsp := s.req(c, req, u) 634 rspe, ok := rsp.(*daemon.APIError) 635 c.Assert(ok, check.Equals, true, check.Commentf("expected apiError resp: %#v", rsp)) 636 return rspe 637 } 638 639 func (s *apiBaseSuite) serveHTTP(c *check.C, w http.ResponseWriter, req *http.Request) { 640 if s.d == nil { 641 panic("call s.daemon(c) etc in your test first") 642 } 643 644 cmd, vars := handlerCommand(c, s.d, req) 645 s.vars = vars 646 647 cmd.ServeHTTP(w, req) 648 } 649 650 func (s *apiBaseSuite) simulateConflict(name string) { 651 if s.d == nil { 652 panic("call s.daemon(c) etc in your test first") 653 } 654 655 o := s.d.Overlord() 656 st := o.State() 657 st.Lock() 658 defer st.Unlock() 659 t := st.NewTask("link-snap", "...") 660 snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{ 661 RealName: name, 662 }} 663 t.Set("snap-setup", snapsup) 664 chg := st.NewChange("manip", "...") 665 chg.AddTask(t) 666 }