github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/daemon/api_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 21 22 import ( 23 "bytes" 24 "context" 25 "crypto" 26 "encoding/json" 27 "errors" 28 "fmt" 29 "io" 30 "io/ioutil" 31 "math" 32 "mime/multipart" 33 "net/http" 34 "net/http/httptest" 35 "net/url" 36 "os" 37 "path/filepath" 38 "regexp" 39 "sort" 40 "strconv" 41 "strings" 42 "time" 43 44 "golang.org/x/crypto/sha3" 45 "gopkg.in/check.v1" 46 "gopkg.in/tomb.v2" 47 48 "github.com/snapcore/snapd/arch" 49 "github.com/snapcore/snapd/asserts" 50 "github.com/snapcore/snapd/asserts/assertstest" 51 "github.com/snapcore/snapd/asserts/sysdb" 52 "github.com/snapcore/snapd/boot" 53 "github.com/snapcore/snapd/client" 54 "github.com/snapcore/snapd/dirs" 55 "github.com/snapcore/snapd/interfaces" 56 "github.com/snapcore/snapd/interfaces/builtin" 57 "github.com/snapcore/snapd/interfaces/ifacetest" 58 "github.com/snapcore/snapd/osutil" 59 "github.com/snapcore/snapd/overlord" 60 "github.com/snapcore/snapd/overlord/assertstate" 61 "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" 62 "github.com/snapcore/snapd/overlord/auth" 63 "github.com/snapcore/snapd/overlord/configstate/config" 64 "github.com/snapcore/snapd/overlord/devicestate" 65 "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" 66 "github.com/snapcore/snapd/overlord/healthstate" 67 "github.com/snapcore/snapd/overlord/hookstate" 68 "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" 69 "github.com/snapcore/snapd/overlord/ifacestate" 70 "github.com/snapcore/snapd/overlord/servicestate" 71 "github.com/snapcore/snapd/overlord/snapstate" 72 "github.com/snapcore/snapd/overlord/state" 73 "github.com/snapcore/snapd/release" 74 "github.com/snapcore/snapd/sandbox" 75 "github.com/snapcore/snapd/snap" 76 "github.com/snapcore/snapd/snap/channel" 77 "github.com/snapcore/snapd/snap/snaptest" 78 "github.com/snapcore/snapd/store" 79 "github.com/snapcore/snapd/store/storetest" 80 "github.com/snapcore/snapd/systemd" 81 "github.com/snapcore/snapd/testutil" 82 ) 83 84 type apiBaseSuite struct { 85 storetest.Store 86 87 rsnaps []*snap.Info 88 err error 89 vars map[string]string 90 storeSearch store.Search 91 suggestedCurrency string 92 d *Daemon 93 user *auth.UserState 94 ctx context.Context 95 restoreBackends func() 96 currentSnaps []*store.CurrentSnap 97 actions []*store.SnapAction 98 buyOptions *client.BuyOptions 99 buyResult *client.BuyResult 100 storeSigning *assertstest.StoreStack 101 restoreRelease func() 102 trustedRestorer func() 103 brands *assertstest.SigningAccounts 104 105 systemctlRestorer func() 106 sysctlBufs [][]byte 107 108 journalctlRestorer func() 109 jctlSvcses [][]string 110 jctlNs []int 111 jctlFollows []bool 112 jctlRCs []io.ReadCloser 113 jctlErrs []error 114 115 serviceControlError error 116 serviceControlCalls []serviceControlArgs 117 118 connectivityResult map[string]bool 119 loginUserStoreMacaroon string 120 loginUserDischarge string 121 userInfoResult *store.User 122 userInfoExpectedEmail string 123 124 restoreSanitize func() 125 126 testutil.BaseTest 127 } 128 129 type serviceControlArgs struct { 130 action string 131 options string 132 names []string 133 } 134 135 func (s *apiBaseSuite) pokeStateLock() { 136 // the store should be called without the state lock held. Try 137 // to acquire it. 138 st := s.d.overlord.State() 139 st.Lock() 140 st.Unlock() 141 } 142 143 func (s *apiBaseSuite) SnapInfo(ctx context.Context, spec store.SnapSpec, user *auth.UserState) (*snap.Info, error) { 144 s.pokeStateLock() 145 s.user = user 146 s.ctx = ctx 147 if len(s.rsnaps) > 0 { 148 return s.rsnaps[0], s.err 149 } 150 return nil, s.err 151 } 152 153 func (s *apiBaseSuite) Find(ctx context.Context, search *store.Search, user *auth.UserState) ([]*snap.Info, error) { 154 s.pokeStateLock() 155 156 s.storeSearch = *search 157 s.user = user 158 s.ctx = ctx 159 160 return s.rsnaps, s.err 161 } 162 163 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) { 164 s.pokeStateLock() 165 if assertQuery != nil { 166 panic("no assertion query support") 167 } 168 169 if ctx == nil { 170 panic("context required") 171 } 172 s.currentSnaps = currentSnaps 173 s.actions = actions 174 s.user = user 175 176 sars := make([]store.SnapActionResult, len(s.rsnaps)) 177 for i, rsnap := range s.rsnaps { 178 sars[i] = store.SnapActionResult{Info: rsnap} 179 } 180 return sars, nil, s.err 181 } 182 183 func (s *apiBaseSuite) SuggestedCurrency() string { 184 s.pokeStateLock() 185 186 return s.suggestedCurrency 187 } 188 189 func (s *apiBaseSuite) Buy(options *client.BuyOptions, user *auth.UserState) (*client.BuyResult, error) { 190 s.pokeStateLock() 191 192 s.buyOptions = options 193 s.user = user 194 return s.buyResult, s.err 195 } 196 197 func (s *apiBaseSuite) ReadyToBuy(user *auth.UserState) error { 198 s.pokeStateLock() 199 200 s.user = user 201 return s.err 202 } 203 204 func (s *apiBaseSuite) ConnectivityCheck() (map[string]bool, error) { 205 s.pokeStateLock() 206 207 return s.connectivityResult, s.err 208 } 209 210 func (s *apiBaseSuite) LoginUser(username, password, otp string) (string, string, error) { 211 s.pokeStateLock() 212 213 return s.loginUserStoreMacaroon, s.loginUserDischarge, s.err 214 } 215 216 func (s *apiBaseSuite) UserInfo(email string) (userinfo *store.User, err error) { 217 s.pokeStateLock() 218 219 if s.userInfoExpectedEmail != email { 220 panic(fmt.Sprintf("%q != %q", s.userInfoExpectedEmail, email)) 221 } 222 return s.userInfoResult, s.err 223 } 224 225 func (s *apiBaseSuite) muxVars(*http.Request) map[string]string { 226 return s.vars 227 } 228 229 func (s *apiBaseSuite) SetUpSuite(c *check.C) { 230 muxVars = s.muxVars 231 s.restoreRelease = sandbox.MockForceDevMode(false) 232 s.systemctlRestorer = systemd.MockSystemctl(s.systemctl) 233 s.journalctlRestorer = systemd.MockJournalctl(s.journalctl) 234 s.restoreSanitize = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) 235 } 236 237 func (s *apiBaseSuite) TearDownSuite(c *check.C) { 238 muxVars = nil 239 s.restoreRelease() 240 s.systemctlRestorer() 241 s.journalctlRestorer() 242 s.restoreSanitize() 243 } 244 245 func (s *apiBaseSuite) systemctl(args ...string) (buf []byte, err error) { 246 if len(s.sysctlBufs) > 0 { 247 buf, s.sysctlBufs = s.sysctlBufs[0], s.sysctlBufs[1:] 248 } 249 250 return buf, err 251 } 252 253 func (s *apiBaseSuite) journalctl(svcs []string, n int, follow bool) (rc io.ReadCloser, err error) { 254 s.jctlSvcses = append(s.jctlSvcses, svcs) 255 s.jctlNs = append(s.jctlNs, n) 256 s.jctlFollows = append(s.jctlFollows, follow) 257 258 if len(s.jctlErrs) > 0 { 259 err, s.jctlErrs = s.jctlErrs[0], s.jctlErrs[1:] 260 } 261 if len(s.jctlRCs) > 0 { 262 rc, s.jctlRCs = s.jctlRCs[0], s.jctlRCs[1:] 263 } 264 265 return rc, err 266 } 267 268 func (s *apiBaseSuite) SetUpTest(c *check.C) { 269 s.sysctlBufs = nil 270 s.jctlSvcses = nil 271 s.jctlNs = nil 272 s.jctlFollows = nil 273 s.jctlRCs = nil 274 s.jctlErrs = nil 275 276 dirs.SetRootDir(c.MkDir()) 277 err := os.MkdirAll(filepath.Dir(dirs.SnapStateFile), 0755) 278 restore := osutil.MockMountInfo("") 279 s.AddCleanup(restore) 280 281 c.Assert(err, check.IsNil) 282 c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), check.IsNil) 283 c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil) 284 285 s.rsnaps = nil 286 s.suggestedCurrency = "" 287 s.storeSearch = store.Search{} 288 s.err = nil 289 s.vars = nil 290 s.user = nil 291 s.d = nil 292 s.currentSnaps = nil 293 s.actions = nil 294 // Disable real security backends for all API tests 295 s.restoreBackends = ifacestate.MockSecurityBackends(nil) 296 297 s.buyOptions = nil 298 s.buyResult = nil 299 300 s.storeSigning = assertstest.NewStoreStack("can0nical", nil) 301 s.trustedRestorer = sysdb.InjectTrusted(s.storeSigning.Trusted) 302 303 s.brands = assertstest.NewSigningAccounts(s.storeSigning) 304 s.brands.Register("my-brand", brandPrivKey, nil) 305 306 assertstateRefreshSnapDeclarations = nil 307 snapstateInstall = nil 308 snapstateInstallMany = nil 309 snapstateInstallPath = nil 310 snapstateRefreshCandidates = nil 311 snapstateRemoveMany = nil 312 snapstateRevert = nil 313 snapstateRevertToRevision = nil 314 snapstateTryPath = nil 315 snapstateUpdate = nil 316 snapstateUpdateMany = nil 317 snapstateSwitch = nil 318 319 devicestateRemodel = nil 320 321 s.serviceControlCalls = nil 322 s.serviceControlError = nil 323 restoreServicestateCtrl := MockServicestateControl(s.fakeServiceControl) 324 s.AddCleanup(restoreServicestateCtrl) 325 } 326 327 func (s *apiBaseSuite) TearDownTest(c *check.C) { 328 s.trustedRestorer() 329 s.d = nil 330 s.ctx = nil 331 s.restoreBackends() 332 unsafeReadSnapInfo = unsafeReadSnapInfoImpl 333 ensureStateSoon = ensureStateSoonImpl 334 dirs.SetRootDir("") 335 336 assertstateRefreshSnapDeclarations = assertstate.RefreshSnapDeclarations 337 snapstateInstall = snapstate.Install 338 snapstateInstallMany = snapstate.InstallMany 339 snapstateInstallPath = snapstate.InstallPath 340 snapstateRefreshCandidates = snapstate.RefreshCandidates 341 snapstateRemoveMany = snapstate.RemoveMany 342 snapstateRevert = snapstate.Revert 343 snapstateRevertToRevision = snapstate.RevertToRevision 344 snapstateTryPath = snapstate.TryPath 345 snapstateUpdate = snapstate.Update 346 snapstateUpdateMany = snapstate.UpdateMany 347 snapstateSwitch = snapstate.Switch 348 } 349 350 var modelDefaults = map[string]interface{}{ 351 "architecture": "amd64", 352 "gadget": "gadget", 353 "kernel": "kernel", 354 } 355 356 func (s *apiBaseSuite) fakeServiceControl(st *state.State, appInfos []*snap.AppInfo, inst *servicestate.Instruction, context *hookstate.Context) ([]*state.TaskSet, error) { 357 st.Lock() 358 defer st.Unlock() 359 360 if s.serviceControlError != nil { 361 return nil, s.serviceControlError 362 } 363 364 serviceCommand := serviceControlArgs{action: inst.Action} 365 if inst.RestartOptions.Reload { 366 serviceCommand.options = "reload" 367 } 368 // only one flag should ever be set (depending on Action), but appending 369 // them below acts as an extra sanity check. 370 if inst.StartOptions.Enable { 371 serviceCommand.options += "enable" 372 } 373 if inst.StopOptions.Disable { 374 serviceCommand.options += "disable" 375 } 376 for _, app := range appInfos { 377 serviceCommand.names = append(serviceCommand.names, fmt.Sprintf("%s.%s", app.Snap.InstanceName(), app.Name)) 378 } 379 s.serviceControlCalls = append(s.serviceControlCalls, serviceCommand) 380 381 t := st.NewTask("dummy", "") 382 ts := state.NewTaskSet(t) 383 return []*state.TaskSet{ts}, nil 384 } 385 386 func (s *apiBaseSuite) mockModel(c *check.C, st *state.State, model *asserts.Model) { 387 // realistic model setup 388 if model == nil { 389 model = s.brands.Model("can0nical", "pc", modelDefaults) 390 } 391 392 snapstate.DeviceCtx = devicestate.DeviceCtx 393 394 assertstatetest.AddMany(st, model) 395 396 devicestatetest.SetDevice(st, &auth.DeviceState{ 397 Brand: model.BrandID(), 398 Model: model.Model(), 399 Serial: "serialserial", 400 }) 401 } 402 403 func (s *apiBaseSuite) daemon(c *check.C) *Daemon { 404 if s.d != nil { 405 panic("called daemon() twice") 406 } 407 d, err := New() 408 c.Assert(err, check.IsNil) 409 d.addRoutes() 410 411 c.Assert(d.overlord.StartUp(), check.IsNil) 412 413 st := d.overlord.State() 414 st.Lock() 415 defer st.Unlock() 416 snapstate.ReplaceStore(st, s) 417 // mark as already seeded 418 st.Set("seeded", true) 419 // registered 420 s.mockModel(c, st, nil) 421 422 // don't actually try to talk to the store on snapstate.Ensure 423 // needs doing after the call to devicestate.Manager (which 424 // happens in daemon.New via overlord.New) 425 snapstate.CanAutoRefresh = nil 426 427 s.d = d 428 return d 429 } 430 431 func (s *apiBaseSuite) daemonWithOverlordMock(c *check.C) *Daemon { 432 if s.d != nil { 433 panic("called daemon() twice") 434 } 435 d, err := New() 436 c.Assert(err, check.IsNil) 437 d.addRoutes() 438 439 o := overlord.Mock() 440 d.overlord = o 441 442 st := d.overlord.State() 443 // adds an assertion db 444 assertstate.Manager(st, o.TaskRunner()) 445 st.Lock() 446 defer st.Unlock() 447 snapstate.ReplaceStore(st, s) 448 449 s.d = d 450 return d 451 } 452 453 type fakeSnapManager struct{} 454 455 func newFakeSnapManager(st *state.State, runner *state.TaskRunner) *fakeSnapManager { 456 runner.AddHandler("fake-install-snap", func(t *state.Task, _ *tomb.Tomb) error { 457 return nil 458 }, nil) 459 runner.AddHandler("fake-install-snap-error", func(t *state.Task, _ *tomb.Tomb) error { 460 return fmt.Errorf("fake-install-snap-error errored") 461 }, nil) 462 463 return &fakeSnapManager{} 464 } 465 466 func (m *fakeSnapManager) Ensure() error { 467 return nil 468 } 469 470 // sanity 471 var _ overlord.StateManager = (*fakeSnapManager)(nil) 472 473 func (s *apiBaseSuite) daemonWithFakeSnapManager(c *check.C) *Daemon { 474 d := s.daemonWithOverlordMock(c) 475 st := d.overlord.State() 476 runner := d.overlord.TaskRunner() 477 d.overlord.AddManager(newFakeSnapManager(st, runner)) 478 d.overlord.AddManager(runner) 479 c.Assert(d.overlord.StartUp(), check.IsNil) 480 return d 481 } 482 483 func (s *apiBaseSuite) waitTrivialChange(c *check.C, chg *state.Change) { 484 err := s.d.overlord.Settle(5 * time.Second) 485 c.Assert(err, check.IsNil) 486 c.Assert(chg.IsReady(), check.Equals, true) 487 } 488 489 func (s *apiBaseSuite) mkInstalledDesktopFile(c *check.C, name, content string) string { 490 df := filepath.Join(dirs.SnapDesktopFilesDir, name) 491 err := os.MkdirAll(filepath.Dir(df), 0755) 492 c.Assert(err, check.IsNil) 493 err = ioutil.WriteFile(df, []byte(content), 0644) 494 c.Assert(err, check.IsNil) 495 return df 496 } 497 498 func (s *apiBaseSuite) mkInstalledInState(c *check.C, daemon *Daemon, instanceName, developer, version string, revision snap.Revision, active bool, extraYaml string) *snap.Info { 499 snapName, instanceKey := snap.SplitInstanceName(instanceName) 500 501 if revision.Local() && developer != "" { 502 panic("not supported") 503 } 504 505 var snapID string 506 if revision.Store() { 507 snapID = snapName + "-id" 508 } 509 // Collect arguments into a snap.SideInfo structure 510 sideInfo := &snap.SideInfo{ 511 SnapID: snapID, 512 RealName: snapName, 513 Revision: revision, 514 Channel: "stable", 515 } 516 517 // Collect other arguments into a yaml string 518 yamlText := fmt.Sprintf(` 519 name: %s 520 version: %s 521 %s`, snapName, version, extraYaml) 522 523 // Mock the snap on disk 524 snapInfo := snaptest.MockSnapInstance(c, instanceName, yamlText, sideInfo) 525 if active { 526 dir, rev := filepath.Split(snapInfo.MountDir()) 527 c.Assert(os.Symlink(rev, dir+"current"), check.IsNil) 528 } 529 c.Assert(snapInfo.InstanceName(), check.Equals, instanceName) 530 531 c.Assert(os.MkdirAll(snapInfo.DataDir(), 0755), check.IsNil) 532 metadir := filepath.Join(snapInfo.MountDir(), "meta") 533 guidir := filepath.Join(metadir, "gui") 534 c.Assert(os.MkdirAll(guidir, 0755), check.IsNil) 535 c.Check(ioutil.WriteFile(filepath.Join(guidir, "icon.svg"), []byte("yadda icon"), 0644), check.IsNil) 536 537 if daemon == nil { 538 return snapInfo 539 } 540 st := daemon.overlord.State() 541 st.Lock() 542 defer st.Unlock() 543 544 var snapst snapstate.SnapState 545 snapstate.Get(st, instanceName, &snapst) 546 snapst.Active = active 547 snapst.Sequence = append(snapst.Sequence, &snapInfo.SideInfo) 548 snapst.Current = snapInfo.SideInfo.Revision 549 snapst.TrackingChannel = "stable" 550 snapst.InstanceKey = instanceKey 551 552 snapstate.Set(st, instanceName, &snapst) 553 554 if developer == "" { 555 return snapInfo 556 } 557 558 devAcct := assertstest.NewAccount(s.storeSigning, developer, map[string]interface{}{ 559 "account-id": developer + "-id", 560 }, "") 561 562 snapInfo.Publisher = snap.StoreAccount{ 563 ID: devAcct.AccountID(), 564 Username: devAcct.Username(), 565 DisplayName: devAcct.DisplayName(), 566 Validation: devAcct.Validation(), 567 } 568 569 snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ 570 "series": "16", 571 "snap-id": snapID, 572 "snap-name": snapName, 573 "publisher-id": devAcct.AccountID(), 574 "timestamp": time.Now().Format(time.RFC3339), 575 }, nil, "") 576 c.Assert(err, check.IsNil) 577 578 content, err := ioutil.ReadFile(snapInfo.MountFile()) 579 c.Assert(err, check.IsNil) 580 h := sha3.Sum384(content) 581 dgst, err := asserts.EncodeDigest(crypto.SHA3_384, h[:]) 582 c.Assert(err, check.IsNil) 583 snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ 584 "snap-sha3-384": string(dgst), 585 "snap-size": "999", 586 "snap-id": snapID, 587 "snap-revision": fmt.Sprintf("%s", revision), 588 "developer-id": devAcct.AccountID(), 589 "timestamp": time.Now().Format(time.RFC3339), 590 }, nil, "") 591 c.Assert(err, check.IsNil) 592 593 assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""), devAcct, snapDecl, snapRev) 594 595 return snapInfo 596 } 597 598 type apiSuite struct { 599 apiBaseSuite 600 } 601 602 var _ = check.Suite(&apiSuite{}) 603 604 func (s *apiSuite) TestUsersOnlyRoot(c *check.C) { 605 for _, cmd := range api { 606 if strings.Contains(cmd.Path, "user") { 607 c.Check(cmd.RootOnly, check.Equals, true, check.Commentf(cmd.Path)) 608 } 609 } 610 } 611 612 func (s *apiSuite) TestSnapInfoOneIntegration(c *check.C) { 613 d := s.daemon(c) 614 s.vars = map[string]string{"name": "foo"} 615 616 // we have v0 [r5] installed 617 s.mkInstalledInState(c, d, "foo", "bar", "v0", snap.R(5), false, "") 618 // and v1 [r10] is current 619 s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, `title: title 620 description: description 621 summary: summary 622 license: GPL-3.0 623 base: base18 624 apps: 625 cmd: 626 command: some.cmd 627 cmd2: 628 command: other.cmd 629 cmd3: 630 command: other.cmd 631 common-id: org.foo.cmd 632 svc1: 633 command: somed1 634 daemon: simple 635 svc2: 636 command: somed2 637 daemon: forking 638 svc3: 639 command: somed3 640 daemon: oneshot 641 svc4: 642 command: somed4 643 daemon: notify 644 svc5: 645 command: some5 646 timer: mon1,12:15 647 daemon: simple 648 svc6: 649 command: some6 650 daemon: simple 651 sockets: 652 sock: 653 listen-stream: $SNAP_COMMON/run.sock 654 svc7: 655 command: some7 656 daemon: simple 657 sockets: 658 other-sock: 659 listen-stream: $SNAP_COMMON/other-run.sock 660 `) 661 df := s.mkInstalledDesktopFile(c, "foo_cmd.desktop", "[Desktop]\nExec=foo.cmd %U") 662 s.sysctlBufs = [][]byte{ 663 []byte(`Type=simple 664 Id=snap.foo.svc1.service 665 ActiveState=fumbling 666 UnitFileState=enabled 667 `), 668 []byte(`Type=forking 669 Id=snap.foo.svc2.service 670 ActiveState=active 671 UnitFileState=disabled 672 `), 673 []byte(`Type=oneshot 674 Id=snap.foo.svc3.service 675 ActiveState=reloading 676 UnitFileState=static 677 `), 678 []byte(`Type=notify 679 Id=snap.foo.svc4.service 680 ActiveState=inactive 681 UnitFileState=potatoes 682 `), 683 []byte(`Type=simple 684 Id=snap.foo.svc5.service 685 ActiveState=inactive 686 UnitFileState=static 687 `), 688 []byte(`Id=snap.foo.svc5.timer 689 ActiveState=active 690 UnitFileState=enabled 691 `), 692 []byte(`Type=simple 693 Id=snap.foo.svc6.service 694 ActiveState=inactive 695 UnitFileState=static 696 `), 697 []byte(`Id=snap.foo.svc6.sock.socket 698 ActiveState=active 699 UnitFileState=enabled 700 `), 701 []byte(`Type=simple 702 Id=snap.foo.svc7.service 703 ActiveState=inactive 704 UnitFileState=static 705 `), 706 []byte(`Id=snap.foo.svc7.other-sock.socket 707 ActiveState=inactive 708 UnitFileState=enabled 709 `), 710 } 711 712 var snapst snapstate.SnapState 713 st := s.d.overlord.State() 714 st.Lock() 715 st.Set("health", map[string]healthstate.HealthState{ 716 "foo": {Status: healthstate.OkayStatus}, 717 }) 718 err := snapstate.Get(st, "foo", &snapst) 719 st.Unlock() 720 c.Assert(err, check.IsNil) 721 722 // modify state 723 snapst.TrackingChannel = "beta" 724 snapst.IgnoreValidation = true 725 snapst.CohortKey = "some-long-cohort-key" 726 st.Lock() 727 snapstate.Set(st, "foo", &snapst) 728 st.Unlock() 729 730 req, err := http.NewRequest("GET", "/v2/snaps/foo", nil) 731 c.Assert(err, check.IsNil) 732 rsp, ok := getSnapInfo(snapCmd, req, nil).(*resp) 733 c.Assert(ok, check.Equals, true) 734 735 c.Assert(rsp, check.NotNil) 736 c.Assert(rsp.Result, check.FitsTypeOf, &client.Snap{}) 737 m := rsp.Result.(*client.Snap) 738 739 // installed-size depends on vagaries of the filesystem, just check type 740 c.Check(m.InstalledSize, check.FitsTypeOf, int64(0)) 741 m.InstalledSize = 0 742 // ditto install-date 743 c.Check(m.InstallDate, check.FitsTypeOf, time.Time{}) 744 m.InstallDate = time.Time{} 745 746 meta := &Meta{} 747 expected := &resp{ 748 Type: ResponseTypeSync, 749 Status: 200, 750 Result: &client.Snap{ 751 ID: "foo-id", 752 Name: "foo", 753 Revision: snap.R(10), 754 Version: "v1", 755 Channel: "stable", 756 TrackingChannel: "beta", 757 IgnoreValidation: true, 758 Title: "title", 759 Summary: "summary", 760 Description: "description", 761 Developer: "bar", 762 Publisher: &snap.StoreAccount{ 763 ID: "bar-id", 764 Username: "bar", 765 DisplayName: "Bar", 766 Validation: "unproven", 767 }, 768 Status: "active", 769 Health: &client.SnapHealth{Status: "okay"}, 770 Icon: "/v2/icons/foo/icon", 771 Type: string(snap.TypeApp), 772 Base: "base18", 773 Private: false, 774 DevMode: false, 775 JailMode: false, 776 Confinement: string(snap.StrictConfinement), 777 TryMode: false, 778 MountedFrom: filepath.Join(dirs.SnapBlobDir, "foo_10.snap"), 779 Apps: []client.AppInfo{ 780 { 781 Snap: "foo", Name: "cmd", 782 DesktopFile: df, 783 }, { 784 // no desktop file 785 Snap: "foo", Name: "cmd2", 786 }, { 787 // has AppStream ID 788 Snap: "foo", Name: "cmd3", 789 CommonID: "org.foo.cmd", 790 }, { 791 // services 792 Snap: "foo", Name: "svc1", 793 Daemon: "simple", 794 Enabled: true, 795 Active: false, 796 }, { 797 Snap: "foo", Name: "svc2", 798 Daemon: "forking", 799 Enabled: false, 800 Active: true, 801 }, { 802 Snap: "foo", Name: "svc3", 803 Daemon: "oneshot", 804 Enabled: true, 805 Active: true, 806 }, { 807 Snap: "foo", Name: "svc4", 808 Daemon: "notify", 809 Enabled: false, 810 Active: false, 811 }, { 812 Snap: "foo", Name: "svc5", 813 Daemon: "simple", 814 Enabled: true, 815 Active: false, 816 Activators: []client.AppActivator{ 817 {Name: "svc5", Type: "timer", Active: true, Enabled: true}, 818 }, 819 }, { 820 Snap: "foo", Name: "svc6", 821 Daemon: "simple", 822 Enabled: true, 823 Active: false, 824 Activators: []client.AppActivator{ 825 {Name: "sock", Type: "socket", Active: true, Enabled: true}, 826 }, 827 }, { 828 Snap: "foo", Name: "svc7", 829 Daemon: "simple", 830 Enabled: true, 831 Active: false, 832 Activators: []client.AppActivator{ 833 {Name: "other-sock", Type: "socket", Active: false, Enabled: true}, 834 }, 835 }, 836 }, 837 Broken: "", 838 Contact: "", 839 License: "GPL-3.0", 840 CommonIDs: []string{"org.foo.cmd"}, 841 CohortKey: "some-long-cohort-key", 842 }, 843 Meta: meta, 844 } 845 846 c.Check(rsp.Result, check.DeepEquals, expected.Result) 847 } 848 849 func (s *apiSuite) TestSnapInfoWithAuth(c *check.C) { 850 s.daemon(c) 851 852 state := snapCmd.d.overlord.State() 853 state.Lock() 854 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 855 state.Unlock() 856 c.Check(err, check.IsNil) 857 858 req, err := http.NewRequest("GET", "/v2/find/?q=name:gfoo", nil) 859 c.Assert(err, check.IsNil) 860 861 c.Assert(s.user, check.IsNil) 862 863 _, ok := searchStore(findCmd, req, user).(*resp) 864 c.Assert(ok, check.Equals, true) 865 // ensure user was set 866 c.Assert(s.user, check.DeepEquals, user) 867 } 868 869 func (s *apiSuite) TestSnapInfoNotFound(c *check.C) { 870 req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) 871 c.Assert(err, check.IsNil) 872 c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, 404) 873 } 874 875 func (s *apiSuite) TestSnapInfoNoneFound(c *check.C) { 876 s.vars = map[string]string{"name": "foo"} 877 878 req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) 879 c.Assert(err, check.IsNil) 880 c.Check(getSnapInfo(snapCmd, req, nil).(*resp).Status, check.Equals, 404) 881 } 882 883 func (s *apiSuite) TestSnapInfoIgnoresRemoteErrors(c *check.C) { 884 s.vars = map[string]string{"name": "foo"} 885 s.err = errors.New("weird") 886 887 req, err := http.NewRequest("GET", "/v2/snaps/gfoo", nil) 888 c.Assert(err, check.IsNil) 889 rsp := getSnapInfo(snapCmd, req, nil).(*resp) 890 891 c.Check(rsp.Type, check.Equals, ResponseTypeError) 892 c.Check(rsp.Status, check.Equals, 404) 893 c.Check(rsp.Result, check.NotNil) 894 } 895 896 func (s *apiSuite) TestMapLocalFields(c *check.C) { 897 media := snap.MediaInfos{ 898 { 899 Type: "screenshot", 900 URL: "https://example.com/shot1.svg", 901 }, { 902 Type: "icon", 903 URL: "https://example.com/icon.png", 904 }, { 905 Type: "screenshot", 906 URL: "https://example.com/shot2.svg", 907 }, 908 } 909 910 publisher := snap.StoreAccount{ 911 ID: "some-dev-id", 912 Username: "some-dev", 913 DisplayName: "Some Developer", 914 Validation: "poor", 915 } 916 info := &snap.Info{ 917 SideInfo: snap.SideInfo{ 918 SnapID: "some-snap-id", 919 RealName: "some-snap", 920 EditedTitle: "A Title", 921 EditedSummary: "a summary", 922 EditedDescription: "the\nlong\ndescription", 923 Channel: "bleeding/edge", 924 Contact: "alice@example.com", 925 Revision: snap.R(7), 926 Private: true, 927 }, 928 InstanceKey: "instance", 929 SnapType: "app", 930 Base: "the-base", 931 Version: "v1.0", 932 License: "MIT", 933 Broken: "very", 934 Confinement: "very strict", 935 CommonIDs: []string{"foo", "bar"}, 936 Media: media, 937 DownloadInfo: snap.DownloadInfo{ 938 Size: 42, 939 Sha3_384: "some-sum", 940 }, 941 Publisher: publisher, 942 } 943 944 // make InstallDate work 945 c.Assert(os.MkdirAll(info.MountDir(), 0755), check.IsNil) 946 c.Assert(os.Symlink("7", filepath.Join(info.MountDir(), "..", "current")), check.IsNil) 947 948 info.Apps = map[string]*snap.AppInfo{ 949 "foo": {Snap: info, Name: "foo", Command: "foo"}, 950 "bar": {Snap: info, Name: "bar", Command: "bar"}, 951 } 952 about := aboutSnap{ 953 info: info, 954 snapst: &snapstate.SnapState{ 955 Active: true, 956 TrackingChannel: "flaky/beta", 957 Current: snap.R(7), 958 Flags: snapstate.Flags{ 959 IgnoreValidation: true, 960 DevMode: true, 961 JailMode: true, 962 }, 963 }, 964 } 965 966 expected := &client.Snap{ 967 ID: "some-snap-id", 968 Name: "some-snap_instance", 969 Summary: "a summary", 970 Description: "the\nlong\ndescription", 971 Developer: "some-dev", 972 Publisher: &publisher, 973 Icon: "https://example.com/icon.png", 974 Type: "app", 975 Base: "the-base", 976 Version: "v1.0", 977 Revision: snap.R(7), 978 Channel: "bleeding/edge", 979 TrackingChannel: "flaky/beta", 980 InstallDate: info.InstallDate(), 981 InstalledSize: 42, 982 Status: "active", 983 Confinement: "very strict", 984 IgnoreValidation: true, 985 DevMode: true, 986 JailMode: true, 987 Private: true, 988 Broken: "very", 989 Contact: "alice@example.com", 990 Title: "A Title", 991 License: "MIT", 992 CommonIDs: []string{"foo", "bar"}, 993 MountedFrom: filepath.Join(dirs.SnapBlobDir, "some-snap_instance_7.snap"), 994 Media: media, 995 Apps: []client.AppInfo{ 996 {Snap: "some-snap_instance", Name: "bar"}, 997 {Snap: "some-snap_instance", Name: "foo"}, 998 }, 999 } 1000 c.Check(mapLocal(about, nil), check.DeepEquals, expected) 1001 } 1002 1003 func (s *apiSuite) TestMapLocalOfTryResolvesSymlink(c *check.C) { 1004 c.Assert(os.MkdirAll(dirs.SnapBlobDir, 0755), check.IsNil) 1005 1006 info := snap.Info{SideInfo: snap.SideInfo{RealName: "hello", Revision: snap.R(1)}} 1007 snapst := snapstate.SnapState{} 1008 mountFile := info.MountFile() 1009 about := aboutSnap{info: &info, snapst: &snapst} 1010 1011 // if not a 'try', then MountedFrom is just MountFile() 1012 c.Check(mapLocal(about, nil).MountedFrom, check.Equals, mountFile) 1013 1014 // if it's a try, then MountedFrom resolves the symlink 1015 // (note it doesn't matter, here, whether the target of the link exists) 1016 snapst.TryMode = true 1017 c.Assert(os.Symlink("/xyzzy", mountFile), check.IsNil) 1018 c.Check(mapLocal(about, nil).MountedFrom, check.Equals, "/xyzzy") 1019 1020 // if the readlink fails, it's unset 1021 c.Assert(os.Remove(mountFile), check.IsNil) 1022 c.Check(mapLocal(about, nil).MountedFrom, check.Equals, "") 1023 } 1024 1025 func (s *apiSuite) TestListIncludesAll(c *check.C) { 1026 // Very basic check to help stop us from not adding all the 1027 // commands to the command list. 1028 found := countCommandDecls(c, check.Commentf("TestListIncludesAll")) 1029 1030 c.Check(found, check.Equals, len(api), 1031 check.Commentf(`At a glance it looks like you've not added all the Commands defined in api to the api list.`)) 1032 } 1033 1034 func (s *apiSuite) TestRootCmd(c *check.C) { 1035 // check it only does GET 1036 c.Check(rootCmd.PUT, check.IsNil) 1037 c.Check(rootCmd.POST, check.IsNil) 1038 c.Assert(rootCmd.GET, check.NotNil) 1039 1040 rec := httptest.NewRecorder() 1041 c.Check(rootCmd.Path, check.Equals, "/") 1042 1043 rootCmd.GET(rootCmd, nil, nil).ServeHTTP(rec, nil) 1044 c.Check(rec.Code, check.Equals, 200) 1045 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") 1046 1047 expected := []interface{}{"TBD"} 1048 var rsp resp 1049 c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) 1050 c.Check(rsp.Status, check.Equals, 200) 1051 c.Check(rsp.Result, check.DeepEquals, expected) 1052 } 1053 1054 func mockSystemdVirt(newVirt string) (restore func()) { 1055 oldVirt := systemdVirt 1056 systemdVirt = newVirt 1057 return func() { systemdVirt = oldVirt } 1058 } 1059 1060 func (s *apiSuite) TestSysInfo(c *check.C) { 1061 // check it only does GET 1062 c.Check(sysInfoCmd.PUT, check.IsNil) 1063 c.Check(sysInfoCmd.POST, check.IsNil) 1064 c.Assert(sysInfoCmd.GET, check.NotNil) 1065 1066 rec := httptest.NewRecorder() 1067 c.Check(sysInfoCmd.Path, check.Equals, "/v2/system-info") 1068 1069 d := s.daemon(c) 1070 d.Version = "42b1" 1071 1072 // set both legacy and new refresh schedules. new one takes priority 1073 st := d.overlord.State() 1074 st.Lock() 1075 tr := config.NewTransaction(st) 1076 tr.Set("core", "refresh.schedule", "00:00-9:00/12:00-13:00") 1077 tr.Set("core", "refresh.timer", "8:00~9:00/2") 1078 tr.Commit() 1079 st.Unlock() 1080 1081 restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"}) 1082 defer restore() 1083 restore = release.MockOnClassic(true) 1084 defer restore() 1085 restore = sandbox.MockForceDevMode(true) 1086 defer restore() 1087 // reload dirs for release info to have effect 1088 dirs.SetRootDir(dirs.GlobalRootDir) 1089 restore = mockSystemdVirt("magic") 1090 defer restore() 1091 1092 buildID := "this-is-my-build-id" 1093 restore = MockBuildID(buildID) 1094 defer restore() 1095 1096 sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil) 1097 c.Check(rec.Code, check.Equals, 200) 1098 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") 1099 1100 expected := map[string]interface{}{ 1101 "series": "16", 1102 "version": "42b1", 1103 "os-release": map[string]interface{}{ 1104 "id": "distro-id", 1105 "version-id": "1.2", 1106 }, 1107 "build-id": buildID, 1108 "on-classic": true, 1109 "managed": false, 1110 "locations": map[string]interface{}{ 1111 "snap-mount-dir": dirs.SnapMountDir, 1112 "snap-bin-dir": dirs.SnapBinariesDir, 1113 }, 1114 "refresh": map[string]interface{}{ 1115 // only the "timer" field 1116 "timer": "8:00~9:00/2", 1117 }, 1118 "confinement": "partial", 1119 "sandbox-features": map[string]interface{}{"confinement-options": []interface{}{"classic", "devmode"}}, 1120 "architecture": arch.DpkgArchitecture(), 1121 "virtualization": "magic", 1122 "system-mode": "run", 1123 } 1124 var rsp resp 1125 c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) 1126 c.Check(rsp.Status, check.Equals, 200) 1127 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1128 // Ensure that we had a kernel-verrsion but don't check the actual value. 1129 const kernelVersionKey = "kernel-version" 1130 c.Check(rsp.Result.(map[string]interface{})[kernelVersionKey], check.Not(check.Equals), "") 1131 delete(rsp.Result.(map[string]interface{}), kernelVersionKey) 1132 c.Check(rsp.Result, check.DeepEquals, expected) 1133 } 1134 1135 func (s *apiSuite) TestSysInfoLegacyRefresh(c *check.C) { 1136 rec := httptest.NewRecorder() 1137 1138 d := s.daemon(c) 1139 d.Version = "42b1" 1140 1141 restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"}) 1142 defer restore() 1143 restore = release.MockOnClassic(true) 1144 defer restore() 1145 restore = sandbox.MockForceDevMode(true) 1146 defer restore() 1147 restore = mockSystemdVirt("kvm") 1148 defer restore() 1149 // reload dirs for release info to have effect 1150 dirs.SetRootDir(dirs.GlobalRootDir) 1151 1152 // set the legacy refresh schedule 1153 st := d.overlord.State() 1154 st.Lock() 1155 tr := config.NewTransaction(st) 1156 tr.Set("core", "refresh.schedule", "00:00-9:00/12:00-13:00") 1157 tr.Set("core", "refresh.timer", "") 1158 tr.Commit() 1159 st.Unlock() 1160 1161 // add a test security backend 1162 err := d.overlord.InterfaceManager().Repository().AddBackend(&ifacetest.TestSecurityBackend{ 1163 BackendName: "apparmor", 1164 SandboxFeaturesCallback: func() []string { return []string{"feature-1", "feature-2"} }, 1165 }) 1166 c.Assert(err, check.IsNil) 1167 1168 buildID := "this-is-my-build-id" 1169 restore = MockBuildID(buildID) 1170 defer restore() 1171 1172 sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil) 1173 c.Check(rec.Code, check.Equals, 200) 1174 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") 1175 1176 expected := map[string]interface{}{ 1177 "series": "16", 1178 "version": "42b1", 1179 "os-release": map[string]interface{}{ 1180 "id": "distro-id", 1181 "version-id": "1.2", 1182 }, 1183 "build-id": buildID, 1184 "on-classic": true, 1185 "managed": false, 1186 "locations": map[string]interface{}{ 1187 "snap-mount-dir": dirs.SnapMountDir, 1188 "snap-bin-dir": dirs.SnapBinariesDir, 1189 }, 1190 "refresh": map[string]interface{}{ 1191 // only the "schedule" field 1192 "schedule": "00:00-9:00/12:00-13:00", 1193 }, 1194 "confinement": "partial", 1195 "sandbox-features": map[string]interface{}{ 1196 "apparmor": []interface{}{"feature-1", "feature-2"}, 1197 "confinement-options": []interface{}{"classic", "devmode"}, // we know it's this because of the release.Mock... calls above 1198 }, 1199 "architecture": arch.DpkgArchitecture(), 1200 "virtualization": "kvm", 1201 "system-mode": "run", 1202 } 1203 var rsp resp 1204 c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) 1205 c.Check(rsp.Status, check.Equals, 200) 1206 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1207 const kernelVersionKey = "kernel-version" 1208 delete(rsp.Result.(map[string]interface{}), kernelVersionKey) 1209 c.Check(rsp.Result, check.DeepEquals, expected) 1210 } 1211 1212 func (s *apiSuite) testSysInfoSystemMode(c *check.C, mode string) { 1213 c.Assert(mode != "", check.Equals, true, check.Commentf("mode is unset for the test")) 1214 rec := httptest.NewRecorder() 1215 1216 restore := release.MockReleaseInfo(&release.OS{ID: "distro-id", VersionID: "1.2"}) 1217 defer restore() 1218 restore = release.MockOnClassic(false) 1219 defer restore() 1220 restore = sandbox.MockForceDevMode(false) 1221 defer restore() 1222 restore = mockSystemdVirt("") 1223 defer restore() 1224 1225 // reload dirs for release info to have effect on paths 1226 dirs.SetRootDir(dirs.GlobalRootDir) 1227 1228 // mock the modeenv file 1229 m := boot.Modeenv{ 1230 Mode: mode, 1231 RecoverySystem: "20191127", 1232 } 1233 err := m.WriteTo("") 1234 c.Assert(err, check.IsNil) 1235 1236 d := s.daemon(c) 1237 d.Version = "42b1" 1238 1239 // add a test security backend 1240 err = d.overlord.InterfaceManager().Repository().AddBackend(&ifacetest.TestSecurityBackend{ 1241 BackendName: "apparmor", 1242 SandboxFeaturesCallback: func() []string { return []string{"feature-1", "feature-2"} }, 1243 }) 1244 c.Assert(err, check.IsNil) 1245 1246 buildID := "this-is-my-build-id" 1247 restore = MockBuildID(buildID) 1248 defer restore() 1249 1250 sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil) 1251 c.Check(rec.Code, check.Equals, 200) 1252 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json") 1253 1254 expected := map[string]interface{}{ 1255 "series": "16", 1256 "version": "42b1", 1257 "os-release": map[string]interface{}{ 1258 "id": "distro-id", 1259 "version-id": "1.2", 1260 }, 1261 "build-id": buildID, 1262 "on-classic": false, 1263 "managed": false, 1264 "locations": map[string]interface{}{ 1265 "snap-mount-dir": dirs.SnapMountDir, 1266 "snap-bin-dir": dirs.SnapBinariesDir, 1267 }, 1268 "refresh": map[string]interface{}{ 1269 "timer": "00:00~24:00/4", 1270 }, 1271 "confinement": "strict", 1272 "sandbox-features": map[string]interface{}{ 1273 "apparmor": []interface{}{"feature-1", "feature-2"}, 1274 "confinement-options": []interface{}{"devmode", "strict"}, // we know it's this because of the release.Mock... calls above 1275 }, 1276 "architecture": arch.DpkgArchitecture(), 1277 "system-mode": mode, 1278 } 1279 var rsp resp 1280 c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) 1281 c.Check(rsp.Status, check.Equals, 200) 1282 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1283 const kernelVersionKey = "kernel-version" 1284 delete(rsp.Result.(map[string]interface{}), kernelVersionKey) 1285 c.Check(rsp.Result, check.DeepEquals, expected) 1286 } 1287 1288 func (s *apiSuite) TestSysInfoSystemModeRun(c *check.C) { 1289 s.testSysInfoSystemMode(c, "run") 1290 } 1291 1292 func (s *apiSuite) TestSysInfoSystemModeRecover(c *check.C) { 1293 s.testSysInfoSystemMode(c, "recover") 1294 } 1295 1296 func (s *apiSuite) TestSysInfoSystemModeInstall(c *check.C) { 1297 s.testSysInfoSystemMode(c, "install") 1298 } 1299 1300 func (s *apiSuite) TestLoginUser(c *check.C) { 1301 d := s.daemon(c) 1302 state := d.overlord.State() 1303 1304 s.loginUserStoreMacaroon = "user-macaroon" 1305 s.loginUserDischarge = "the-discharge-macaroon-serialized-data" 1306 buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) 1307 req, err := http.NewRequest("POST", "/v2/login", buf) 1308 c.Assert(err, check.IsNil) 1309 1310 rsp := loginUser(loginCmd, req, nil).(*resp) 1311 1312 state.Lock() 1313 user, err := auth.User(state, 1) 1314 state.Unlock() 1315 c.Check(err, check.IsNil) 1316 1317 expected := userResponseData{ 1318 ID: 1, 1319 Email: "email@.com", 1320 1321 Macaroon: user.Macaroon, 1322 Discharges: user.Discharges, 1323 } 1324 1325 c.Check(rsp.Status, check.Equals, 200) 1326 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1327 c.Assert(rsp.Result, check.FitsTypeOf, expected) 1328 c.Check(rsp.Result, check.DeepEquals, expected) 1329 1330 c.Check(user.ID, check.Equals, 1) 1331 c.Check(user.Username, check.Equals, "") 1332 c.Check(user.Email, check.Equals, "email@.com") 1333 c.Check(user.Discharges, check.IsNil) 1334 c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon) 1335 c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) 1336 // snapd macaroon was setup too 1337 snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon) 1338 c.Check(err, check.IsNil) 1339 c.Check(snapdMacaroon.Id(), check.Equals, "1") 1340 c.Check(snapdMacaroon.Location(), check.Equals, "snapd") 1341 } 1342 1343 func (s *apiSuite) TestLoginUserWithUsername(c *check.C) { 1344 d := s.daemon(c) 1345 state := d.overlord.State() 1346 1347 s.loginUserStoreMacaroon = "user-macaroon" 1348 s.loginUserDischarge = "the-discharge-macaroon-serialized-data" 1349 buf := bytes.NewBufferString(`{"username": "username", "email": "email@.com", "password": "password"}`) 1350 req, err := http.NewRequest("POST", "/v2/login", buf) 1351 c.Assert(err, check.IsNil) 1352 1353 rsp := loginUser(loginCmd, req, nil).(*resp) 1354 1355 state.Lock() 1356 user, err := auth.User(state, 1) 1357 state.Unlock() 1358 c.Check(err, check.IsNil) 1359 1360 expected := userResponseData{ 1361 ID: 1, 1362 Username: "username", 1363 Email: "email@.com", 1364 Macaroon: user.Macaroon, 1365 Discharges: user.Discharges, 1366 } 1367 c.Check(rsp.Status, check.Equals, 200) 1368 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1369 c.Assert(rsp.Result, check.FitsTypeOf, expected) 1370 c.Check(rsp.Result, check.DeepEquals, expected) 1371 1372 c.Check(user.ID, check.Equals, 1) 1373 c.Check(user.Username, check.Equals, "username") 1374 c.Check(user.Email, check.Equals, "email@.com") 1375 c.Check(user.Discharges, check.IsNil) 1376 c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon) 1377 c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) 1378 // snapd macaroon was setup too 1379 snapdMacaroon, err := auth.MacaroonDeserialize(user.Macaroon) 1380 c.Check(err, check.IsNil) 1381 c.Check(snapdMacaroon.Id(), check.Equals, "1") 1382 c.Check(snapdMacaroon.Location(), check.Equals, "snapd") 1383 } 1384 1385 func (s *apiSuite) TestLoginUserNoEmailWithExistentLocalUser(c *check.C) { 1386 d := s.daemon(c) 1387 state := d.overlord.State() 1388 1389 // setup local-only user 1390 state.Lock() 1391 localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) 1392 state.Unlock() 1393 c.Assert(err, check.IsNil) 1394 1395 s.loginUserStoreMacaroon = "user-macaroon" 1396 s.loginUserDischarge = "the-discharge-macaroon-serialized-data" 1397 buf := bytes.NewBufferString(`{"username": "username", "email": "", "password": "password"}`) 1398 req, err := http.NewRequest("POST", "/v2/login", buf) 1399 c.Assert(err, check.IsNil) 1400 req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) 1401 1402 rsp := loginUser(loginCmd, req, localUser).(*resp) 1403 1404 expected := userResponseData{ 1405 ID: 1, 1406 Username: "username", 1407 Email: "email@test.com", 1408 1409 Macaroon: localUser.Macaroon, 1410 Discharges: localUser.Discharges, 1411 } 1412 c.Check(rsp.Status, check.Equals, 200) 1413 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1414 c.Assert(rsp.Result, check.FitsTypeOf, expected) 1415 c.Check(rsp.Result, check.DeepEquals, expected) 1416 1417 state.Lock() 1418 user, err := auth.User(state, localUser.ID) 1419 state.Unlock() 1420 c.Check(err, check.IsNil) 1421 c.Check(user.Username, check.Equals, "username") 1422 c.Check(user.Email, check.Equals, localUser.Email) 1423 c.Check(user.Macaroon, check.Equals, localUser.Macaroon) 1424 c.Check(user.Discharges, check.IsNil) 1425 c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon) 1426 c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) 1427 } 1428 1429 func (s *apiSuite) TestLoginUserWithExistentLocalUser(c *check.C) { 1430 d := s.daemon(c) 1431 state := d.overlord.State() 1432 1433 // setup local-only user 1434 state.Lock() 1435 localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) 1436 state.Unlock() 1437 c.Assert(err, check.IsNil) 1438 1439 s.loginUserStoreMacaroon = "user-macaroon" 1440 s.loginUserDischarge = "the-discharge-macaroon-serialized-data" 1441 buf := bytes.NewBufferString(`{"username": "username", "email": "email@test.com", "password": "password"}`) 1442 req, err := http.NewRequest("POST", "/v2/login", buf) 1443 c.Assert(err, check.IsNil) 1444 req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) 1445 1446 rsp := loginUser(loginCmd, req, localUser).(*resp) 1447 1448 expected := userResponseData{ 1449 ID: 1, 1450 Username: "username", 1451 Email: "email@test.com", 1452 1453 Macaroon: localUser.Macaroon, 1454 Discharges: localUser.Discharges, 1455 } 1456 c.Check(rsp.Status, check.Equals, 200) 1457 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1458 c.Assert(rsp.Result, check.FitsTypeOf, expected) 1459 c.Check(rsp.Result, check.DeepEquals, expected) 1460 1461 state.Lock() 1462 user, err := auth.User(state, localUser.ID) 1463 state.Unlock() 1464 c.Check(err, check.IsNil) 1465 c.Check(user.Username, check.Equals, "username") 1466 c.Check(user.Email, check.Equals, localUser.Email) 1467 c.Check(user.Macaroon, check.Equals, localUser.Macaroon) 1468 c.Check(user.Discharges, check.IsNil) 1469 c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon) 1470 c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) 1471 } 1472 1473 func (s *apiSuite) TestLoginUserNewEmailWithExistentLocalUser(c *check.C) { 1474 d := s.daemon(c) 1475 state := d.overlord.State() 1476 1477 // setup local-only user 1478 state.Lock() 1479 localUser, err := auth.NewUser(state, "username", "email@test.com", "", nil) 1480 state.Unlock() 1481 c.Assert(err, check.IsNil) 1482 1483 s.loginUserStoreMacaroon = "user-macaroon" 1484 s.loginUserDischarge = "the-discharge-macaroon-serialized-data" 1485 // same local user, but using a new SSO account 1486 buf := bytes.NewBufferString(`{"username": "username", "email": "new.email@test.com", "password": "password"}`) 1487 req, err := http.NewRequest("POST", "/v2/login", buf) 1488 c.Assert(err, check.IsNil) 1489 req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, localUser.Macaroon)) 1490 1491 rsp := loginUser(loginCmd, req, localUser).(*resp) 1492 1493 expected := userResponseData{ 1494 ID: 1, 1495 Username: "username", 1496 Email: "new.email@test.com", 1497 1498 Macaroon: localUser.Macaroon, 1499 Discharges: localUser.Discharges, 1500 } 1501 c.Check(rsp.Status, check.Equals, 200) 1502 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1503 c.Assert(rsp.Result, check.FitsTypeOf, expected) 1504 c.Check(rsp.Result, check.DeepEquals, expected) 1505 1506 state.Lock() 1507 user, err := auth.User(state, localUser.ID) 1508 state.Unlock() 1509 c.Check(err, check.IsNil) 1510 c.Check(user.Username, check.Equals, "username") 1511 c.Check(user.Email, check.Equals, expected.Email) 1512 c.Check(user.Macaroon, check.Equals, localUser.Macaroon) 1513 c.Check(user.Discharges, check.IsNil) 1514 c.Check(user.StoreMacaroon, check.Equals, s.loginUserStoreMacaroon) 1515 c.Check(user.StoreDischarges, check.DeepEquals, []string{"the-discharge-macaroon-serialized-data"}) 1516 } 1517 1518 func (s *apiSuite) TestLogoutUser(c *check.C) { 1519 d := s.daemon(c) 1520 state := d.overlord.State() 1521 state.Lock() 1522 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 1523 state.Unlock() 1524 c.Assert(err, check.IsNil) 1525 1526 req, err := http.NewRequest("POST", "/v2/logout", nil) 1527 c.Assert(err, check.IsNil) 1528 req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) 1529 1530 rsp := logoutUser(logoutCmd, req, user).(*resp) 1531 c.Check(rsp.Status, check.Equals, 200) 1532 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1533 1534 state.Lock() 1535 _, err = auth.User(state, user.ID) 1536 state.Unlock() 1537 c.Check(err, check.Equals, auth.ErrInvalidUser) 1538 } 1539 1540 func (s *apiSuite) TestLoginUserBadRequest(c *check.C) { 1541 buf := bytes.NewBufferString(`hello`) 1542 req, err := http.NewRequest("POST", "/v2/login", buf) 1543 c.Assert(err, check.IsNil) 1544 1545 rsp := loginUser(snapCmd, req, nil).(*resp) 1546 1547 c.Check(rsp.Type, check.Equals, ResponseTypeError) 1548 c.Check(rsp.Status, check.Equals, 400) 1549 c.Check(rsp.Result, check.NotNil) 1550 } 1551 1552 func (s *apiSuite) TestLoginUserDeveloperAPIError(c *check.C) { 1553 s.daemon(c) 1554 1555 s.err = fmt.Errorf("error-from-login-user") 1556 buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) 1557 req, err := http.NewRequest("POST", "/v2/login", buf) 1558 c.Assert(err, check.IsNil) 1559 1560 rsp := loginUser(snapCmd, req, nil).(*resp) 1561 1562 c.Check(rsp.Type, check.Equals, ResponseTypeError) 1563 c.Check(rsp.Status, check.Equals, 401) 1564 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "error-from-login-user") 1565 } 1566 1567 func (s *apiSuite) TestLoginUserTwoFactorRequiredError(c *check.C) { 1568 s.daemon(c) 1569 1570 s.err = store.ErrAuthenticationNeeds2fa 1571 buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) 1572 req, err := http.NewRequest("POST", "/v2/login", buf) 1573 c.Assert(err, check.IsNil) 1574 1575 rsp := loginUser(snapCmd, req, nil).(*resp) 1576 1577 c.Check(rsp.Type, check.Equals, ResponseTypeError) 1578 c.Check(rsp.Status, check.Equals, 401) 1579 c.Check(rsp.Result.(*errorResult).Kind, check.Equals, client.ErrorKindTwoFactorRequired) 1580 } 1581 1582 func (s *apiSuite) TestLoginUserTwoFactorFailedError(c *check.C) { 1583 s.daemon(c) 1584 1585 s.err = store.Err2faFailed 1586 buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) 1587 req, err := http.NewRequest("POST", "/v2/login", buf) 1588 c.Assert(err, check.IsNil) 1589 1590 rsp := loginUser(snapCmd, req, nil).(*resp) 1591 1592 c.Check(rsp.Type, check.Equals, ResponseTypeError) 1593 c.Check(rsp.Status, check.Equals, 401) 1594 c.Check(rsp.Result.(*errorResult).Kind, check.Equals, client.ErrorKindTwoFactorFailed) 1595 } 1596 1597 func (s *apiSuite) TestLoginUserInvalidCredentialsError(c *check.C) { 1598 s.daemon(c) 1599 1600 s.err = store.ErrInvalidCredentials 1601 buf := bytes.NewBufferString(`{"username": "email@.com", "password": "password"}`) 1602 req, err := http.NewRequest("POST", "/v2/login", buf) 1603 c.Assert(err, check.IsNil) 1604 1605 rsp := loginUser(snapCmd, req, nil).(*resp) 1606 1607 c.Check(rsp.Type, check.Equals, ResponseTypeError) 1608 c.Check(rsp.Status, check.Equals, 401) 1609 c.Check(rsp.Result.(*errorResult).Message, check.Equals, "invalid credentials") 1610 } 1611 1612 func (s *apiSuite) TestUserFromRequestNoHeader(c *check.C) { 1613 req, _ := http.NewRequest("GET", "http://example.com", nil) 1614 1615 state := snapCmd.d.overlord.State() 1616 state.Lock() 1617 user, err := UserFromRequest(state, req) 1618 state.Unlock() 1619 1620 c.Check(err, check.Equals, auth.ErrInvalidAuth) 1621 c.Check(user, check.IsNil) 1622 } 1623 1624 func (s *apiSuite) TestUserFromRequestHeaderNoMacaroons(c *check.C) { 1625 req, _ := http.NewRequest("GET", "http://example.com", nil) 1626 req.Header.Set("Authorization", "Invalid") 1627 1628 state := snapCmd.d.overlord.State() 1629 state.Lock() 1630 user, err := UserFromRequest(state, req) 1631 state.Unlock() 1632 1633 c.Check(err, check.ErrorMatches, "authorization header misses Macaroon prefix") 1634 c.Check(user, check.IsNil) 1635 } 1636 1637 func (s *apiSuite) TestUserFromRequestHeaderIncomplete(c *check.C) { 1638 req, _ := http.NewRequest("GET", "http://example.com", nil) 1639 req.Header.Set("Authorization", `Macaroon root=""`) 1640 1641 state := snapCmd.d.overlord.State() 1642 state.Lock() 1643 user, err := UserFromRequest(state, req) 1644 state.Unlock() 1645 1646 c.Check(err, check.ErrorMatches, "invalid authorization header") 1647 c.Check(user, check.IsNil) 1648 } 1649 1650 func (s *apiSuite) TestUserFromRequestHeaderCorrectMissingUser(c *check.C) { 1651 req, _ := http.NewRequest("GET", "http://example.com", nil) 1652 req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) 1653 1654 state := snapCmd.d.overlord.State() 1655 state.Lock() 1656 user, err := UserFromRequest(state, req) 1657 state.Unlock() 1658 1659 c.Check(err, check.Equals, auth.ErrInvalidAuth) 1660 c.Check(user, check.IsNil) 1661 } 1662 1663 func (s *apiSuite) TestUserFromRequestHeaderValidUser(c *check.C) { 1664 state := snapCmd.d.overlord.State() 1665 state.Lock() 1666 expectedUser, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 1667 state.Unlock() 1668 c.Check(err, check.IsNil) 1669 1670 req, _ := http.NewRequest("GET", "http://example.com", nil) 1671 req.Header.Set("Authorization", fmt.Sprintf(`Macaroon root="%s"`, expectedUser.Macaroon)) 1672 1673 state.Lock() 1674 user, err := UserFromRequest(state, req) 1675 state.Unlock() 1676 1677 c.Check(err, check.IsNil) 1678 c.Check(user, check.DeepEquals, expectedUser) 1679 } 1680 1681 func (s *apiSuite) TestSnapsInfoOnePerIntegration(c *check.C) { 1682 s.checkSnapInfoOnePerIntegration(c, false, nil) 1683 } 1684 1685 func (s *apiSuite) TestSnapsInfoOnePerIntegrationSome(c *check.C) { 1686 s.checkSnapInfoOnePerIntegration(c, false, []string{"foo", "baz"}) 1687 } 1688 1689 func (s *apiSuite) TestSnapsInfoOnePerIntegrationAll(c *check.C) { 1690 s.checkSnapInfoOnePerIntegration(c, true, nil) 1691 } 1692 1693 func (s *apiSuite) TestSnapsInfoOnePerIntegrationAllSome(c *check.C) { 1694 s.checkSnapInfoOnePerIntegration(c, true, []string{"foo", "baz"}) 1695 } 1696 1697 func (s *apiSuite) checkSnapInfoOnePerIntegration(c *check.C, all bool, names []string) { 1698 d := s.daemon(c) 1699 1700 type tsnap struct { 1701 name string 1702 dev string 1703 ver string 1704 rev int 1705 active bool 1706 1707 wanted bool 1708 } 1709 1710 tsnaps := []tsnap{ 1711 {name: "foo", dev: "bar", ver: "v0.9", rev: 1}, 1712 {name: "foo", dev: "bar", ver: "v1", rev: 5, active: true}, 1713 {name: "bar", dev: "baz", ver: "v2", rev: 10, active: true}, 1714 {name: "baz", dev: "qux", ver: "v3", rev: 15, active: true}, 1715 {name: "qux", dev: "mip", ver: "v4", rev: 20, active: true}, 1716 } 1717 numExpected := 0 1718 1719 for _, snp := range tsnaps { 1720 if all || snp.active { 1721 if len(names) == 0 { 1722 numExpected++ 1723 snp.wanted = true 1724 } 1725 for _, n := range names { 1726 if snp.name == n { 1727 numExpected++ 1728 snp.wanted = true 1729 break 1730 } 1731 } 1732 } 1733 s.mkInstalledInState(c, d, snp.name, snp.dev, snp.ver, snap.R(snp.rev), snp.active, "") 1734 } 1735 1736 q := url.Values{} 1737 if all { 1738 q.Set("select", "all") 1739 } 1740 if len(names) > 0 { 1741 q.Set("snaps", strings.Join(names, ",")) 1742 } 1743 req, err := http.NewRequest("GET", "/v2/snaps?"+q.Encode(), nil) 1744 c.Assert(err, check.IsNil) 1745 1746 rsp, ok := getSnapsInfo(snapsCmd, req, nil).(*resp) 1747 c.Assert(ok, check.Equals, true) 1748 1749 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 1750 c.Check(rsp.Status, check.Equals, 200) 1751 c.Check(rsp.Result, check.NotNil) 1752 1753 snaps := snapList(rsp.Result) 1754 c.Check(snaps, check.HasLen, numExpected) 1755 1756 for _, s := range tsnaps { 1757 if !((all || s.active) && s.wanted) { 1758 continue 1759 } 1760 var got map[string]interface{} 1761 for _, got = range snaps { 1762 if got["name"].(string) == s.name && got["revision"].(string) == snap.R(s.rev).String() { 1763 break 1764 } 1765 } 1766 c.Check(got["name"], check.Equals, s.name) 1767 c.Check(got["version"], check.Equals, s.ver) 1768 c.Check(got["revision"], check.Equals, snap.R(s.rev).String()) 1769 c.Check(got["developer"], check.Equals, s.dev) 1770 c.Check(got["confinement"], check.Equals, "strict") 1771 } 1772 } 1773 1774 func (s *apiSuite) TestSnapsInfoOnlyLocal(c *check.C) { 1775 d := s.daemon(c) 1776 1777 s.rsnaps = []*snap.Info{{ 1778 SideInfo: snap.SideInfo{ 1779 RealName: "store", 1780 }, 1781 Publisher: snap.StoreAccount{ 1782 ID: "foo-id", 1783 Username: "foo", 1784 DisplayName: "Foo", 1785 Validation: "unproven", 1786 }, 1787 }} 1788 s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") 1789 st := s.d.overlord.State() 1790 st.Lock() 1791 st.Set("health", map[string]healthstate.HealthState{ 1792 "local": {Status: healthstate.OkayStatus}, 1793 }) 1794 st.Unlock() 1795 1796 req, err := http.NewRequest("GET", "/v2/snaps?sources=local", nil) 1797 c.Assert(err, check.IsNil) 1798 1799 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 1800 1801 c.Assert(rsp.Sources, check.DeepEquals, []string{"local"}) 1802 1803 snaps := snapList(rsp.Result) 1804 c.Assert(snaps, check.HasLen, 1) 1805 c.Assert(snaps[0]["name"], check.Equals, "local") 1806 c.Check(snaps[0]["health"], check.DeepEquals, map[string]interface{}{ 1807 "status": "okay", 1808 "revision": "unset", 1809 "timestamp": "0001-01-01T00:00:00Z", 1810 }) 1811 } 1812 1813 func (s *apiSuite) TestSnapsInfoAllMixedPublishers(c *check.C) { 1814 d := s.daemon(c) 1815 1816 // the first 'local' is from a 'local' snap 1817 s.mkInstalledInState(c, d, "local", "", "v1", snap.R(-1), false, "") 1818 s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(1), false, "") 1819 s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(2), true, "") 1820 1821 req, err := http.NewRequest("GET", "/v2/snaps?select=all", nil) 1822 c.Assert(err, check.IsNil) 1823 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 1824 c.Assert(rsp.Type, check.Equals, ResponseTypeSync) 1825 1826 snaps := snapList(rsp.Result) 1827 c.Assert(snaps, check.HasLen, 3) 1828 1829 publisher := map[string]interface{}{ 1830 "id": "foo-id", 1831 "username": "foo", 1832 "display-name": "Foo", 1833 "validation": "unproven", 1834 } 1835 1836 c.Check(snaps[0]["publisher"], check.IsNil) 1837 c.Check(snaps[1]["publisher"], check.DeepEquals, publisher) 1838 c.Check(snaps[2]["publisher"], check.DeepEquals, publisher) 1839 } 1840 1841 func (s *apiSuite) TestSnapsInfoAll(c *check.C) { 1842 d := s.daemon(c) 1843 1844 s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(1), false, "") 1845 s.mkInstalledInState(c, d, "local", "foo", "v2", snap.R(2), false, "") 1846 s.mkInstalledInState(c, d, "local", "foo", "v3", snap.R(3), true, "") 1847 s.mkInstalledInState(c, d, "local_foo", "foo", "v4", snap.R(4), true, "") 1848 brokenInfo := s.mkInstalledInState(c, d, "local_bar", "foo", "v5", snap.R(5), true, "") 1849 // make sure local_bar is 'broken' 1850 err := os.Remove(filepath.Join(brokenInfo.MountDir(), "meta", "snap.yaml")) 1851 c.Assert(err, check.IsNil) 1852 1853 expectedHappy := map[string]bool{ 1854 "local": true, 1855 "local_foo": true, 1856 "local_bar": true, 1857 } 1858 for _, t := range []struct { 1859 q string 1860 numSnaps int 1861 typ ResponseType 1862 }{ 1863 {"?select=enabled", 3, "sync"}, 1864 {`?select=`, 3, "sync"}, 1865 {"", 3, "sync"}, 1866 {"?select=all", 5, "sync"}, 1867 {"?select=invalid-field", 0, "error"}, 1868 } { 1869 c.Logf("trying: %v", t) 1870 req, err := http.NewRequest("GET", fmt.Sprintf("/v2/snaps%s", t.q), nil) 1871 c.Assert(err, check.IsNil) 1872 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 1873 c.Assert(rsp.Type, check.Equals, t.typ) 1874 1875 if rsp.Type != "error" { 1876 snaps := snapList(rsp.Result) 1877 c.Assert(snaps, check.HasLen, t.numSnaps) 1878 seen := map[string]bool{} 1879 for _, s := range snaps { 1880 seen[s["name"].(string)] = true 1881 } 1882 c.Assert(seen, check.DeepEquals, expectedHappy) 1883 } 1884 } 1885 } 1886 1887 func (s *apiSuite) TestFind(c *check.C) { 1888 s.daemon(c) 1889 1890 s.suggestedCurrency = "EUR" 1891 1892 s.rsnaps = []*snap.Info{{ 1893 SideInfo: snap.SideInfo{ 1894 RealName: "store", 1895 }, 1896 Publisher: snap.StoreAccount{ 1897 ID: "foo-id", 1898 Username: "foo", 1899 DisplayName: "Foo", 1900 Validation: "unproven", 1901 }, 1902 }} 1903 1904 req, err := http.NewRequest("GET", "/v2/find?q=hi", nil) 1905 c.Assert(err, check.IsNil) 1906 1907 rsp := searchStore(findCmd, req, nil).(*resp) 1908 1909 snaps := snapList(rsp.Result) 1910 c.Assert(snaps, check.HasLen, 1) 1911 c.Assert(snaps[0]["name"], check.Equals, "store") 1912 c.Check(snaps[0]["prices"], check.IsNil) 1913 c.Check(snaps[0]["channels"], check.IsNil) 1914 1915 c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") 1916 1917 c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "hi"}) 1918 c.Check(s.currentSnaps, check.HasLen, 0) 1919 c.Check(s.actions, check.HasLen, 0) 1920 } 1921 1922 func (s *apiSuite) TestFindRefreshes(c *check.C) { 1923 snapstateRefreshCandidates = snapstate.RefreshCandidates 1924 s.daemon(c) 1925 1926 s.rsnaps = []*snap.Info{{ 1927 SideInfo: snap.SideInfo{ 1928 RealName: "store", 1929 }, 1930 Publisher: snap.StoreAccount{ 1931 ID: "foo-id", 1932 Username: "foo", 1933 DisplayName: "Foo", 1934 Validation: "unproven", 1935 }, 1936 }} 1937 s.mockSnap(c, "name: store\nversion: 1.0") 1938 1939 req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) 1940 c.Assert(err, check.IsNil) 1941 1942 rsp := searchStore(findCmd, req, nil).(*resp) 1943 1944 snaps := snapList(rsp.Result) 1945 c.Assert(snaps, check.HasLen, 1) 1946 c.Assert(snaps[0]["name"], check.Equals, "store") 1947 c.Check(s.currentSnaps, check.HasLen, 1) 1948 c.Check(s.actions, check.HasLen, 1) 1949 } 1950 1951 func (s *apiSuite) TestFindRefreshSideloaded(c *check.C) { 1952 snapstateRefreshCandidates = snapstate.RefreshCandidates 1953 s.daemon(c) 1954 1955 s.rsnaps = []*snap.Info{{ 1956 SideInfo: snap.SideInfo{ 1957 RealName: "store", 1958 }, 1959 Publisher: snap.StoreAccount{ 1960 ID: "foo-id", 1961 Username: "foo", 1962 DisplayName: "Foo", 1963 Validation: "unproven", 1964 }, 1965 }} 1966 1967 s.mockSnap(c, "name: store\nversion: 1.0") 1968 1969 var snapst snapstate.SnapState 1970 st := s.d.overlord.State() 1971 st.Lock() 1972 err := snapstate.Get(st, "store", &snapst) 1973 st.Unlock() 1974 c.Assert(err, check.IsNil) 1975 c.Assert(snapst.Sequence, check.HasLen, 1) 1976 1977 // clear the snapid 1978 snapst.Sequence[0].SnapID = "" 1979 st.Lock() 1980 snapstate.Set(st, "store", &snapst) 1981 st.Unlock() 1982 1983 req, err := http.NewRequest("GET", "/v2/find?select=refresh", nil) 1984 c.Assert(err, check.IsNil) 1985 1986 rsp := searchStore(findCmd, req, nil).(*resp) 1987 1988 snaps := snapList(rsp.Result) 1989 c.Assert(snaps, check.HasLen, 0) 1990 c.Check(s.currentSnaps, check.HasLen, 0) 1991 c.Check(s.actions, check.HasLen, 0) 1992 } 1993 1994 func (s *apiSuite) TestFindPrivate(c *check.C) { 1995 s.daemon(c) 1996 1997 s.rsnaps = []*snap.Info{} 1998 1999 req, err := http.NewRequest("GET", "/v2/find?q=foo&select=private", nil) 2000 c.Assert(err, check.IsNil) 2001 2002 _ = searchStore(findCmd, req, nil).(*resp) 2003 2004 c.Check(s.storeSearch, check.DeepEquals, store.Search{ 2005 Query: "foo", 2006 Private: true, 2007 }) 2008 } 2009 2010 func (s *apiSuite) TestFindUserAgentContextCreated(c *check.C) { 2011 s.daemon(c) 2012 2013 req, err := http.NewRequest("GET", "/v2/find", nil) 2014 c.Assert(err, check.IsNil) 2015 req.Header.Add("User-Agent", "some-agent/1.0") 2016 2017 _ = searchStore(findCmd, req, nil).(*resp) 2018 2019 c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0") 2020 } 2021 2022 func (s *apiSuite) TestFindOneUserAgentContextCreated(c *check.C) { 2023 s.daemon(c) 2024 2025 s.rsnaps = []*snap.Info{{ 2026 SnapType: snap.TypeApp, 2027 Version: "v2", 2028 SideInfo: snap.SideInfo{ 2029 RealName: "banana", 2030 }, 2031 Publisher: snap.StoreAccount{ 2032 ID: "foo-id", 2033 Username: "foo", 2034 DisplayName: "Foo", 2035 Validation: "unproven", 2036 }, 2037 }} 2038 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 2039 c.Assert(err, check.IsNil) 2040 req.Header.Add("User-Agent", "some-agent/1.0") 2041 2042 _ = searchStore(findCmd, req, nil).(*resp) 2043 2044 c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0") 2045 } 2046 2047 func (s *apiSuite) TestFindPrefix(c *check.C) { 2048 s.daemon(c) 2049 2050 s.rsnaps = []*snap.Info{} 2051 2052 req, err := http.NewRequest("GET", "/v2/find?name=foo*", nil) 2053 c.Assert(err, check.IsNil) 2054 2055 _ = searchStore(findCmd, req, nil).(*resp) 2056 2057 c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo", Prefix: true}) 2058 } 2059 2060 func (s *apiSuite) TestFindSection(c *check.C) { 2061 s.daemon(c) 2062 2063 s.rsnaps = []*snap.Info{} 2064 2065 req, err := http.NewRequest("GET", "/v2/find?q=foo§ion=bar", nil) 2066 c.Assert(err, check.IsNil) 2067 2068 _ = searchStore(findCmd, req, nil).(*resp) 2069 2070 c.Check(s.storeSearch, check.DeepEquals, store.Search{ 2071 Query: "foo", 2072 Category: "bar", 2073 }) 2074 } 2075 2076 func (s *apiSuite) TestFindScope(c *check.C) { 2077 s.daemon(c) 2078 2079 s.rsnaps = []*snap.Info{} 2080 2081 req, err := http.NewRequest("GET", "/v2/find?q=foo&scope=creep", nil) 2082 c.Assert(err, check.IsNil) 2083 2084 _ = searchStore(findCmd, req, nil).(*resp) 2085 2086 c.Check(s.storeSearch, check.DeepEquals, store.Search{ 2087 Query: "foo", 2088 Scope: "creep", 2089 }) 2090 } 2091 2092 func (s *apiSuite) TestFindCommonID(c *check.C) { 2093 s.daemon(c) 2094 2095 s.rsnaps = []*snap.Info{{ 2096 SideInfo: snap.SideInfo{ 2097 RealName: "store", 2098 }, 2099 Publisher: snap.StoreAccount{ 2100 ID: "foo-id", 2101 Username: "foo", 2102 DisplayName: "Foo", 2103 Validation: "unproven", 2104 }, 2105 CommonIDs: []string{"org.foo"}, 2106 }} 2107 s.mockSnap(c, "name: store\nversion: 1.0") 2108 2109 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 2110 c.Assert(err, check.IsNil) 2111 2112 rsp := searchStore(findCmd, req, nil).(*resp) 2113 2114 snaps := snapList(rsp.Result) 2115 c.Assert(snaps, check.HasLen, 1) 2116 c.Check(snaps[0]["common-ids"], check.DeepEquals, []interface{}{"org.foo"}) 2117 } 2118 2119 func (s *apiSuite) TestFindByCommonID(c *check.C) { 2120 s.daemon(c) 2121 2122 s.rsnaps = []*snap.Info{{ 2123 SideInfo: snap.SideInfo{ 2124 RealName: "store", 2125 }, 2126 Publisher: snap.StoreAccount{ 2127 ID: "foo-id", 2128 Username: "foo", 2129 DisplayName: "Foo", 2130 Validation: "unproven", 2131 }, 2132 CommonIDs: []string{"org.foo"}, 2133 }} 2134 s.mockSnap(c, "name: store\nversion: 1.0") 2135 2136 req, err := http.NewRequest("GET", "/v2/find?common-id=org.foo", nil) 2137 c.Assert(err, check.IsNil) 2138 2139 rsp := searchStore(findCmd, req, nil).(*resp) 2140 2141 snaps := snapList(rsp.Result) 2142 c.Assert(snaps, check.HasLen, 1) 2143 c.Check(s.storeSearch, check.DeepEquals, store.Search{CommonID: "org.foo"}) 2144 } 2145 2146 func (s *apiSuite) TestFindOne(c *check.C) { 2147 s.daemon(c) 2148 2149 s.rsnaps = []*snap.Info{{ 2150 SideInfo: snap.SideInfo{ 2151 RealName: "store", 2152 }, 2153 Base: "base0", 2154 Publisher: snap.StoreAccount{ 2155 ID: "foo-id", 2156 Username: "foo", 2157 DisplayName: "Foo", 2158 Validation: "verified", 2159 }, 2160 Channels: map[string]*snap.ChannelSnapInfo{ 2161 "stable": { 2162 Revision: snap.R(42), 2163 }, 2164 }, 2165 }} 2166 s.mockSnap(c, "name: store\nversion: 1.0") 2167 2168 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 2169 c.Assert(err, check.IsNil) 2170 2171 rsp := searchStore(findCmd, req, nil).(*resp) 2172 2173 c.Check(s.storeSearch, check.DeepEquals, store.Search{}) 2174 2175 snaps := snapList(rsp.Result) 2176 c.Assert(snaps, check.HasLen, 1) 2177 c.Check(snaps[0]["name"], check.Equals, "store") 2178 c.Check(snaps[0]["base"], check.Equals, "base0") 2179 c.Check(snaps[0]["publisher"], check.DeepEquals, map[string]interface{}{ 2180 "id": "foo-id", 2181 "username": "foo", 2182 "display-name": "Foo", 2183 "validation": "verified", 2184 }) 2185 m := snaps[0]["channels"].(map[string]interface{})["stable"].(map[string]interface{}) 2186 2187 c.Check(m["revision"], check.Equals, "42") 2188 } 2189 2190 func (s *apiSuite) TestFindOneNotFound(c *check.C) { 2191 s.daemon(c) 2192 2193 s.err = store.ErrSnapNotFound 2194 s.mockSnap(c, "name: store\nversion: 1.0") 2195 2196 req, err := http.NewRequest("GET", "/v2/find?name=foo", nil) 2197 c.Assert(err, check.IsNil) 2198 2199 rsp := searchStore(findCmd, req, nil).(*resp) 2200 2201 c.Check(s.storeSearch, check.DeepEquals, store.Search{}) 2202 c.Check(rsp.Status, check.Equals, 404) 2203 } 2204 2205 func (s *apiSuite) TestFindRefreshNotOther(c *check.C) { 2206 for _, other := range []string{"name", "q", "common-id"} { 2207 req, err := http.NewRequest("GET", "/v2/find?select=refresh&"+other+"=foo*", nil) 2208 c.Assert(err, check.IsNil) 2209 2210 rsp := searchStore(findCmd, req, nil).(*resp) 2211 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2212 c.Check(rsp.Status, check.Equals, 400) 2213 c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot use '"+other+"' with 'select=refresh'") 2214 } 2215 } 2216 2217 func (s *apiSuite) TestFindNotTogether(c *check.C) { 2218 queries := map[string]string{"q": "foo", "name": "foo*", "common-id": "foo"} 2219 for ki, vi := range queries { 2220 for kj, vj := range queries { 2221 if ki == kj { 2222 continue 2223 } 2224 2225 req, err := http.NewRequest("GET", fmt.Sprintf("/v2/find?%s=%s&%s=%s", ki, vi, kj, vj), nil) 2226 c.Assert(err, check.IsNil) 2227 2228 rsp := searchStore(findCmd, req, nil).(*resp) 2229 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2230 c.Check(rsp.Status, check.Equals, 400) 2231 exp1 := "cannot use '" + ki + "' and '" + kj + "' together" 2232 exp2 := "cannot use '" + kj + "' and '" + ki + "' together" 2233 c.Check(rsp.Result.(*errorResult).Message, check.Matches, exp1+"|"+exp2) 2234 } 2235 } 2236 } 2237 2238 func (s *apiSuite) TestFindBadQueryReturnsCorrectErrorKind(c *check.C) { 2239 s.daemon(c) 2240 2241 s.err = store.ErrBadQuery 2242 req, err := http.NewRequest("GET", "/v2/find?q=return-bad-query-please", nil) 2243 c.Assert(err, check.IsNil) 2244 2245 rsp := searchStore(findCmd, req, nil).(*resp) 2246 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2247 c.Check(rsp.Status, check.Equals, 400) 2248 c.Check(rsp.Result.(*errorResult).Message, check.Matches, "bad query") 2249 c.Check(rsp.Result.(*errorResult).Kind, check.Equals, client.ErrorKindBadQuery) 2250 } 2251 2252 func (s *apiSuite) TestFindPriced(c *check.C) { 2253 s.daemon(c) 2254 2255 s.suggestedCurrency = "GBP" 2256 2257 s.rsnaps = []*snap.Info{{ 2258 SnapType: snap.TypeApp, 2259 Version: "v2", 2260 Prices: map[string]float64{ 2261 "GBP": 1.23, 2262 "EUR": 2.34, 2263 }, 2264 MustBuy: true, 2265 SideInfo: snap.SideInfo{ 2266 RealName: "banana", 2267 }, 2268 Publisher: snap.StoreAccount{ 2269 ID: "foo-id", 2270 Username: "foo", 2271 DisplayName: "Foo", 2272 Validation: "unproven", 2273 }, 2274 }} 2275 2276 req, err := http.NewRequest("GET", "/v2/find?q=banana&channel=stable", nil) 2277 c.Assert(err, check.IsNil) 2278 rsp, ok := searchStore(findCmd, req, nil).(*resp) 2279 c.Assert(ok, check.Equals, true) 2280 2281 snaps := snapList(rsp.Result) 2282 c.Assert(snaps, check.HasLen, 1) 2283 2284 snap := snaps[0] 2285 c.Check(snap["name"], check.Equals, "banana") 2286 c.Check(snap["prices"], check.DeepEquals, map[string]interface{}{ 2287 "EUR": 2.34, 2288 "GBP": 1.23, 2289 }) 2290 c.Check(snap["status"], check.Equals, "priced") 2291 2292 c.Check(rsp.SuggestedCurrency, check.Equals, "GBP") 2293 } 2294 2295 func (s *apiSuite) TestFindScreenshotted(c *check.C) { 2296 s.daemon(c) 2297 2298 s.rsnaps = []*snap.Info{{ 2299 SnapType: snap.TypeApp, 2300 Version: "v2", 2301 Media: []snap.MediaInfo{ 2302 { 2303 Type: "screenshot", 2304 URL: "http://example.com/screenshot.png", 2305 Width: 800, 2306 Height: 1280, 2307 }, 2308 { 2309 Type: "screenshot", 2310 URL: "http://example.com/screenshot2.png", 2311 }, 2312 }, 2313 MustBuy: true, 2314 SideInfo: snap.SideInfo{ 2315 RealName: "test-screenshot", 2316 }, 2317 Publisher: snap.StoreAccount{ 2318 ID: "foo-id", 2319 Username: "foo", 2320 DisplayName: "Foo", 2321 Validation: "unproven", 2322 }, 2323 }} 2324 2325 req, err := http.NewRequest("GET", "/v2/find?q=test-screenshot", nil) 2326 c.Assert(err, check.IsNil) 2327 rsp, ok := searchStore(findCmd, req, nil).(*resp) 2328 c.Assert(ok, check.Equals, true) 2329 2330 snaps := snapList(rsp.Result) 2331 c.Assert(snaps, check.HasLen, 1) 2332 2333 c.Check(snaps[0]["name"], check.Equals, "test-screenshot") 2334 c.Check(snaps[0]["media"], check.DeepEquals, []interface{}{ 2335 map[string]interface{}{ 2336 "type": "screenshot", 2337 "url": "http://example.com/screenshot.png", 2338 "width": float64(800), 2339 "height": float64(1280), 2340 }, 2341 map[string]interface{}{ 2342 "type": "screenshot", 2343 "url": "http://example.com/screenshot2.png", 2344 }, 2345 }) 2346 } 2347 2348 func (s *apiSuite) TestSnapsInfoOnlyStore(c *check.C) { 2349 d := s.daemon(c) 2350 2351 s.suggestedCurrency = "EUR" 2352 2353 s.rsnaps = []*snap.Info{{ 2354 SideInfo: snap.SideInfo{ 2355 RealName: "store", 2356 }, 2357 Publisher: snap.StoreAccount{ 2358 ID: "foo-id", 2359 Username: "foo", 2360 DisplayName: "Foo", 2361 Validation: "unproven", 2362 }, 2363 }} 2364 s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") 2365 2366 req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil) 2367 c.Assert(err, check.IsNil) 2368 2369 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 2370 2371 c.Assert(rsp.Sources, check.DeepEquals, []string{"store"}) 2372 2373 snaps := snapList(rsp.Result) 2374 c.Assert(snaps, check.HasLen, 1) 2375 c.Assert(snaps[0]["name"], check.Equals, "store") 2376 c.Check(snaps[0]["prices"], check.IsNil) 2377 2378 c.Check(rsp.SuggestedCurrency, check.Equals, "EUR") 2379 } 2380 2381 func (s *apiSuite) TestSnapsStoreConfinement(c *check.C) { 2382 s.daemon(c) 2383 2384 s.rsnaps = []*snap.Info{ 2385 { 2386 // no explicit confinement in this one 2387 SideInfo: snap.SideInfo{ 2388 RealName: "foo", 2389 }, 2390 }, 2391 { 2392 Confinement: snap.StrictConfinement, 2393 SideInfo: snap.SideInfo{ 2394 RealName: "bar", 2395 }, 2396 }, 2397 { 2398 Confinement: snap.DevModeConfinement, 2399 SideInfo: snap.SideInfo{ 2400 RealName: "baz", 2401 }, 2402 }, 2403 } 2404 2405 req, err := http.NewRequest("GET", "/v2/find", nil) 2406 c.Assert(err, check.IsNil) 2407 2408 rsp := searchStore(findCmd, req, nil).(*resp) 2409 2410 snaps := snapList(rsp.Result) 2411 c.Assert(snaps, check.HasLen, 3) 2412 2413 for i, ss := range [][2]string{ 2414 {"foo", string(snap.StrictConfinement)}, 2415 {"bar", string(snap.StrictConfinement)}, 2416 {"baz", string(snap.DevModeConfinement)}, 2417 } { 2418 name, mode := ss[0], ss[1] 2419 c.Check(snaps[i]["name"], check.Equals, name, check.Commentf(name)) 2420 c.Check(snaps[i]["confinement"], check.Equals, mode, check.Commentf(name)) 2421 } 2422 } 2423 2424 func (s *apiSuite) TestSnapsInfoStoreWithAuth(c *check.C) { 2425 s.daemon(c) 2426 2427 state := snapCmd.d.overlord.State() 2428 state.Lock() 2429 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 2430 state.Unlock() 2431 c.Check(err, check.IsNil) 2432 2433 req, err := http.NewRequest("GET", "/v2/snaps?sources=store", nil) 2434 c.Assert(err, check.IsNil) 2435 2436 c.Assert(s.user, check.IsNil) 2437 2438 _ = getSnapsInfo(snapsCmd, req, user).(*resp) 2439 2440 // ensure user was set 2441 c.Assert(s.user, check.DeepEquals, user) 2442 } 2443 2444 func (s *apiSuite) TestSnapsInfoLocalAndStore(c *check.C) { 2445 d := s.daemon(c) 2446 2447 s.rsnaps = []*snap.Info{{ 2448 Version: "v42", 2449 SideInfo: snap.SideInfo{ 2450 RealName: "remote", 2451 }, 2452 Publisher: snap.StoreAccount{ 2453 ID: "foo-id", 2454 Username: "foo", 2455 DisplayName: "Foo", 2456 Validation: "unproven", 2457 }, 2458 }} 2459 s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") 2460 2461 req, err := http.NewRequest("GET", "/v2/snaps?sources=local,store", nil) 2462 c.Assert(err, check.IsNil) 2463 2464 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 2465 2466 // presence of 'store' in sources bounces request over to /find 2467 c.Assert(rsp.Sources, check.DeepEquals, []string{"store"}) 2468 2469 snaps := snapList(rsp.Result) 2470 c.Assert(snaps, check.HasLen, 1) 2471 c.Check(snaps[0]["version"], check.Equals, "v42") 2472 2473 // as does a 'q' 2474 req, err = http.NewRequest("GET", "/v2/snaps?q=what", nil) 2475 c.Assert(err, check.IsNil) 2476 rsp = getSnapsInfo(snapsCmd, req, nil).(*resp) 2477 snaps = snapList(rsp.Result) 2478 c.Assert(snaps, check.HasLen, 1) 2479 c.Check(snaps[0]["version"], check.Equals, "v42") 2480 2481 // otherwise, local only 2482 req, err = http.NewRequest("GET", "/v2/snaps", nil) 2483 c.Assert(err, check.IsNil) 2484 rsp = getSnapsInfo(snapsCmd, req, nil).(*resp) 2485 snaps = snapList(rsp.Result) 2486 c.Assert(snaps, check.HasLen, 1) 2487 c.Check(snaps[0]["version"], check.Equals, "v1") 2488 } 2489 2490 func (s *apiSuite) TestSnapsInfoDefaultSources(c *check.C) { 2491 d := s.daemon(c) 2492 2493 s.rsnaps = []*snap.Info{{ 2494 SideInfo: snap.SideInfo{ 2495 RealName: "remote", 2496 }, 2497 Publisher: snap.StoreAccount{ 2498 ID: "foo-id", 2499 Username: "foo", 2500 DisplayName: "Foo", 2501 Validation: "unproven", 2502 }, 2503 }} 2504 s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "") 2505 2506 req, err := http.NewRequest("GET", "/v2/snaps", nil) 2507 c.Assert(err, check.IsNil) 2508 2509 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 2510 2511 c.Assert(rsp.Sources, check.DeepEquals, []string{"local"}) 2512 snaps := snapList(rsp.Result) 2513 c.Assert(snaps, check.HasLen, 1) 2514 } 2515 2516 func (s *apiSuite) TestSnapsInfoFilterRemote(c *check.C) { 2517 s.daemon(c) 2518 2519 s.rsnaps = nil 2520 2521 req, err := http.NewRequest("GET", "/v2/snaps?q=foo&sources=store", nil) 2522 c.Assert(err, check.IsNil) 2523 2524 rsp := getSnapsInfo(snapsCmd, req, nil).(*resp) 2525 2526 c.Check(s.storeSearch, check.DeepEquals, store.Search{Query: "foo"}) 2527 2528 c.Assert(rsp.Result, check.NotNil) 2529 } 2530 2531 func (s *apiSuite) TestPostSnapBadRequest(c *check.C) { 2532 buf := bytes.NewBufferString(`hello`) 2533 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2534 c.Assert(err, check.IsNil) 2535 2536 rsp := postSnap(snapCmd, req, nil).(*resp) 2537 2538 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2539 c.Check(rsp.Status, check.Equals, 400) 2540 c.Check(rsp.Result, check.NotNil) 2541 } 2542 2543 func (s *apiSuite) TestPostSnapBadAction(c *check.C) { 2544 buf := bytes.NewBufferString(`{"action": "potato"}`) 2545 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2546 c.Assert(err, check.IsNil) 2547 2548 rsp := postSnap(snapCmd, req, nil).(*resp) 2549 2550 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2551 c.Check(rsp.Status, check.Equals, 400) 2552 c.Check(rsp.Result, check.NotNil) 2553 } 2554 2555 func (s *apiSuite) TestPostSnapBadChannel(c *check.C) { 2556 buf := bytes.NewBufferString(`{"channel": "1/2/3/4"}`) 2557 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2558 c.Assert(err, check.IsNil) 2559 2560 rsp := postSnap(snapCmd, req, nil).(*resp) 2561 2562 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2563 c.Check(rsp.Status, check.Equals, 400) 2564 c.Check(rsp.Result, check.NotNil) 2565 } 2566 2567 func (s *apiSuite) TestPostSnap(c *check.C) { 2568 s.testPostSnap(c, false) 2569 } 2570 2571 func (s *apiSuite) TestPostSnapWithChannel(c *check.C) { 2572 s.testPostSnap(c, true) 2573 } 2574 2575 func (s *apiSuite) testPostSnap(c *check.C, withChannel bool) { 2576 d := s.daemonWithOverlordMock(c) 2577 2578 soon := 0 2579 ensureStateSoon = func(st *state.State) { 2580 soon++ 2581 ensureStateSoonImpl(st) 2582 } 2583 2584 s.vars = map[string]string{"name": "foo"} 2585 2586 snapInstructionDispTable["install"] = func(inst *snapInstruction, _ *state.State) (string, []*state.TaskSet, error) { 2587 if withChannel { 2588 // channel in -> channel out 2589 c.Check(inst.Channel, check.Equals, "xyzzy") 2590 } else { 2591 // no channel in -> no channel out 2592 c.Check(inst.Channel, check.Equals, "") 2593 } 2594 return "foooo", nil, nil 2595 } 2596 defer func() { 2597 snapInstructionDispTable["install"] = snapInstall 2598 }() 2599 2600 var buf *bytes.Buffer 2601 if withChannel { 2602 buf = bytes.NewBufferString(`{"action": "install", "channel": "xyzzy"}`) 2603 } else { 2604 buf = bytes.NewBufferString(`{"action": "install"}`) 2605 } 2606 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2607 c.Assert(err, check.IsNil) 2608 2609 rsp := postSnap(snapCmd, req, nil).(*resp) 2610 2611 c.Check(rsp.Type, check.Equals, ResponseTypeAsync) 2612 2613 st := d.overlord.State() 2614 st.Lock() 2615 defer st.Unlock() 2616 chg := st.Change(rsp.Change) 2617 c.Assert(chg, check.NotNil) 2618 c.Check(chg.Summary(), check.Equals, "foooo") 2619 var names []string 2620 err = chg.Get("snap-names", &names) 2621 c.Assert(err, check.IsNil) 2622 c.Check(names, check.DeepEquals, []string{"foo"}) 2623 2624 c.Check(soon, check.Equals, 1) 2625 } 2626 2627 func (s *apiSuite) TestPostSnapChannel(c *check.C) { 2628 d := s.daemonWithOverlordMock(c) 2629 2630 soon := 0 2631 ensureStateSoon = func(st *state.State) { 2632 soon++ 2633 ensureStateSoonImpl(st) 2634 } 2635 2636 s.vars = map[string]string{"name": "foo"} 2637 2638 snapInstructionDispTable["install"] = func(*snapInstruction, *state.State) (string, []*state.TaskSet, error) { 2639 return "foooo", nil, nil 2640 } 2641 defer func() { 2642 snapInstructionDispTable["install"] = snapInstall 2643 }() 2644 2645 buf := bytes.NewBufferString(`{"action": "install"}`) 2646 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2647 c.Assert(err, check.IsNil) 2648 2649 rsp := postSnap(snapCmd, req, nil).(*resp) 2650 2651 c.Check(rsp.Type, check.Equals, ResponseTypeAsync) 2652 2653 st := d.overlord.State() 2654 st.Lock() 2655 defer st.Unlock() 2656 chg := st.Change(rsp.Change) 2657 c.Assert(chg, check.NotNil) 2658 c.Check(chg.Summary(), check.Equals, "foooo") 2659 var names []string 2660 err = chg.Get("snap-names", &names) 2661 c.Assert(err, check.IsNil) 2662 c.Check(names, check.DeepEquals, []string{"foo"}) 2663 2664 c.Check(soon, check.Equals, 1) 2665 } 2666 2667 func (s *apiSuite) TestPostSnapVerifySnapInstruction(c *check.C) { 2668 s.daemonWithOverlordMock(c) 2669 2670 buf := bytes.NewBufferString(`{"action": "install"}`) 2671 req, err := http.NewRequest("POST", "/v2/snaps/ubuntu-core", buf) 2672 c.Assert(err, check.IsNil) 2673 s.vars = map[string]string{"name": "ubuntu-core"} 2674 2675 rsp := postSnap(snapCmd, req, nil).(*resp) 2676 2677 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2678 c.Check(rsp.Status, check.Equals, 400) 2679 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, `cannot install "ubuntu-core", please use "core" instead`) 2680 } 2681 2682 func (s *apiSuite) TestPostSnapCohortRandoAction(c *check.C) { 2683 s.daemonWithOverlordMock(c) 2684 s.vars = map[string]string{"name": "some-snap"} 2685 const expectedErr = "cohort-key can only be specified for install, refresh, or switch" 2686 2687 for _, action := range []string{"remove", "revert", "enable", "disable", "xyzzy"} { 2688 buf := strings.NewReader(fmt.Sprintf(`{"action": "%s", "cohort-key": "32"}`, action)) 2689 req, err := http.NewRequest("POST", "/v2/snaps/some-snap", buf) 2690 c.Assert(err, check.IsNil) 2691 2692 rsp := postSnap(snapCmd, req, nil).(*resp) 2693 2694 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2695 c.Check(rsp.Status, check.Equals, 400, check.Commentf("%q", action)) 2696 c.Check(rsp.Result.(*errorResult).Message, check.Equals, expectedErr, check.Commentf("%q", action)) 2697 } 2698 } 2699 2700 func (s *apiSuite) TestPostSnapLeaveCohortRandoAction(c *check.C) { 2701 s.daemonWithOverlordMock(c) 2702 s.vars = map[string]string{"name": "some-snap"} 2703 const expectedErr = "leave-cohort can only be specified for refresh or switch" 2704 2705 for _, action := range []string{"install", "remove", "revert", "enable", "disable", "xyzzy"} { 2706 buf := strings.NewReader(fmt.Sprintf(`{"action": "%s", "leave-cohort": true}`, action)) 2707 req, err := http.NewRequest("POST", "/v2/snaps/some-snap", buf) 2708 c.Assert(err, check.IsNil) 2709 2710 rsp := postSnap(snapCmd, req, nil).(*resp) 2711 2712 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2713 c.Check(rsp.Status, check.Equals, 400, check.Commentf("%q", action)) 2714 c.Check(rsp.Result.(*errorResult).Message, check.Equals, expectedErr, check.Commentf("%q", action)) 2715 } 2716 } 2717 2718 func (s *apiSuite) TestPostSnapCohortIncompat(c *check.C) { 2719 s.daemonWithOverlordMock(c) 2720 s.vars = map[string]string{"name": "some-snap"} 2721 2722 type T struct { 2723 opts string 2724 errmsg string 2725 } 2726 2727 for i, t := range []T{ 2728 // TODO: more? 2729 {`"cohort-key": "what", "revision": "42"`, `cannot specify both cohort-key and revision`}, 2730 {`"cohort-key": "what", "leave-cohort": true`, `cannot specify both cohort-key and leave-cohort`}, 2731 } { 2732 buf := strings.NewReader(fmt.Sprintf(`{"action": "refresh", %s}`, t.opts)) 2733 req, err := http.NewRequest("POST", "/v2/snaps/some-snap", buf) 2734 c.Assert(err, check.IsNil, check.Commentf("%d (%s)", i, t.opts)) 2735 2736 rsp := postSnap(snapCmd, req, nil).(*resp) 2737 2738 c.Check(rsp.Type, check.Equals, ResponseTypeError, check.Commentf("%d (%s)", i, t.opts)) 2739 c.Check(rsp.Status, check.Equals, 400, check.Commentf("%d (%s)", i, t.opts)) 2740 c.Check(rsp.Result.(*errorResult).Message, check.Equals, t.errmsg, check.Commentf("%d (%s)", i, t.opts)) 2741 } 2742 } 2743 2744 func (s *apiSuite) TestPostSnapVerifyMultiSnapInstruction(c *check.C) { 2745 s.daemonWithOverlordMock(c) 2746 2747 buf := strings.NewReader(`{"action": "install","snaps":["ubuntu-core"]}`) 2748 req, err := http.NewRequest("POST", "/v2/snaps", buf) 2749 c.Assert(err, check.IsNil) 2750 req.Header.Set("Content-Type", "application/json") 2751 2752 rsp := postSnaps(snapsCmd, req, nil).(*resp) 2753 2754 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2755 c.Check(rsp.Status, check.Equals, 400) 2756 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, `cannot install "ubuntu-core", please use "core" instead`) 2757 } 2758 2759 func (s *apiSuite) TestPostSnapsNoWeirdses(c *check.C) { 2760 s.daemonWithOverlordMock(c) 2761 2762 // one could add more actions here ... 🤷 2763 for _, action := range []string{"install", "refresh", "remove"} { 2764 for weird, v := range map[string]string{ 2765 "channel": `"beta"`, 2766 "revision": `"1"`, 2767 "devmode": "true", 2768 "jailmode": "true", 2769 "cohort-key": `"what"`, 2770 "leave-cohort": "true", 2771 "purge": "true", 2772 } { 2773 buf := strings.NewReader(fmt.Sprintf(`{"action": "%s","snaps":["foo","bar"], "%s": %s}`, action, weird, v)) 2774 req, err := http.NewRequest("POST", "/v2/snaps", buf) 2775 c.Assert(err, check.IsNil) 2776 req.Header.Set("Content-Type", "application/json") 2777 2778 rsp := postSnaps(snapsCmd, req, nil).(*resp) 2779 2780 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2781 c.Check(rsp.Status, check.Equals, 400) 2782 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, `unsupported option provided for multi-snap operation`) 2783 } 2784 } 2785 } 2786 2787 func (s *apiSuite) TestPostSnapSetsUser(c *check.C) { 2788 d := s.daemon(c) 2789 ensureStateSoon = func(st *state.State) {} 2790 2791 snapInstructionDispTable["install"] = func(inst *snapInstruction, st *state.State) (string, []*state.TaskSet, error) { 2792 return fmt.Sprintf("<install by user %d>", inst.userID), nil, nil 2793 } 2794 defer func() { 2795 snapInstructionDispTable["install"] = snapInstall 2796 }() 2797 2798 state := snapCmd.d.overlord.State() 2799 state.Lock() 2800 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 2801 state.Unlock() 2802 c.Check(err, check.IsNil) 2803 2804 buf := bytes.NewBufferString(`{"action": "install"}`) 2805 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2806 c.Assert(err, check.IsNil) 2807 req.Header.Set("Authorization", `Macaroon root="macaroon", discharge="discharge"`) 2808 2809 rsp := postSnap(snapCmd, req, user).(*resp) 2810 2811 c.Check(rsp.Type, check.Equals, ResponseTypeAsync) 2812 2813 st := d.overlord.State() 2814 st.Lock() 2815 defer st.Unlock() 2816 chg := st.Change(rsp.Change) 2817 c.Assert(chg, check.NotNil) 2818 c.Check(chg.Summary(), check.Equals, "<install by user 1>") 2819 } 2820 2821 func (s *apiSuite) TestPostSnapDispatch(c *check.C) { 2822 inst := &snapInstruction{Snaps: []string{"foo"}} 2823 2824 type T struct { 2825 s string 2826 impl snapActionFunc 2827 } 2828 2829 actions := []T{ 2830 {"install", snapInstall}, 2831 {"refresh", snapUpdate}, 2832 {"remove", snapRemove}, 2833 {"revert", snapRevert}, 2834 {"enable", snapEnable}, 2835 {"disable", snapDisable}, 2836 {"switch", snapSwitch}, 2837 {"xyzzy", nil}, 2838 } 2839 2840 for _, action := range actions { 2841 inst.Action = action.s 2842 // do you feel dirty yet? 2843 c.Check(fmt.Sprintf("%p", action.impl), check.Equals, fmt.Sprintf("%p", inst.dispatch())) 2844 } 2845 } 2846 2847 func (s *apiSuite) TestPostSnapEnableDisableSwitchRevision(c *check.C) { 2848 for _, action := range []string{"enable", "disable", "switch"} { 2849 buf := bytes.NewBufferString(`{"action": "` + action + `", "revision": "42"}`) 2850 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 2851 c.Assert(err, check.IsNil) 2852 2853 rsp := postSnap(snapCmd, req, nil).(*resp) 2854 2855 c.Check(rsp.Type, check.Equals, ResponseTypeError) 2856 c.Check(rsp.Status, check.Equals, 400) 2857 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "takes no revision") 2858 } 2859 } 2860 2861 var sideLoadBodyWithoutDevMode = "" + 2862 "----hello--\r\n" + 2863 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 2864 "\r\n" + 2865 "xyzzy\r\n" + 2866 "----hello--\r\n" + 2867 "Content-Disposition: form-data; name=\"dangerous\"\r\n" + 2868 "\r\n" + 2869 "true\r\n" + 2870 "----hello--\r\n" + 2871 "Content-Disposition: form-data; name=\"snap-path\"\r\n" + 2872 "\r\n" + 2873 "a/b/local.snap\r\n" + 2874 "----hello--\r\n" 2875 2876 func (s *apiSuite) TestSideloadSnapOnNonDevModeDistro(c *check.C) { 2877 // try a multipart/form-data upload 2878 body := sideLoadBodyWithoutDevMode 2879 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 2880 chgSummary := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true}) 2881 c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) 2882 } 2883 2884 func (s *apiSuite) TestSideloadSnapOnDevModeDistro(c *check.C) { 2885 // try a multipart/form-data upload 2886 body := sideLoadBodyWithoutDevMode 2887 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 2888 restore := sandbox.MockForceDevMode(true) 2889 defer restore() 2890 flags := snapstate.Flags{RemoveSnapPath: true} 2891 chgSummary := s.sideloadCheck(c, body, head, "local", flags) 2892 c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) 2893 } 2894 2895 func (s *apiSuite) TestSideloadSnapDevMode(c *check.C) { 2896 body := "" + 2897 "----hello--\r\n" + 2898 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 2899 "\r\n" + 2900 "xyzzy\r\n" + 2901 "----hello--\r\n" + 2902 "Content-Disposition: form-data; name=\"devmode\"\r\n" + 2903 "\r\n" + 2904 "true\r\n" + 2905 "----hello--\r\n" 2906 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 2907 // try a multipart/form-data upload 2908 flags := snapstate.Flags{RemoveSnapPath: true} 2909 flags.DevMode = true 2910 chgSummary := s.sideloadCheck(c, body, head, "local", flags) 2911 c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) 2912 } 2913 2914 func (s *apiSuite) TestSideloadSnapJailMode(c *check.C) { 2915 body := "" + 2916 "----hello--\r\n" + 2917 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 2918 "\r\n" + 2919 "xyzzy\r\n" + 2920 "----hello--\r\n" + 2921 "Content-Disposition: form-data; name=\"jailmode\"\r\n" + 2922 "\r\n" + 2923 "true\r\n" + 2924 "----hello--\r\n" + 2925 "Content-Disposition: form-data; name=\"dangerous\"\r\n" + 2926 "\r\n" + 2927 "true\r\n" + 2928 "----hello--\r\n" 2929 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 2930 // try a multipart/form-data upload 2931 flags := snapstate.Flags{JailMode: true, RemoveSnapPath: true} 2932 chgSummary := s.sideloadCheck(c, body, head, "local", flags) 2933 c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) 2934 } 2935 2936 func (s *apiSuite) sideloadCheck(c *check.C, content string, head map[string]string, expectedInstanceName string, expectedFlags snapstate.Flags) string { 2937 d := s.daemonWithFakeSnapManager(c) 2938 2939 soon := 0 2940 ensureStateSoon = func(st *state.State) { 2941 soon++ 2942 ensureStateSoonImpl(st) 2943 } 2944 2945 c.Assert(expectedInstanceName != "", check.Equals, true, check.Commentf("expected instance name must be set")) 2946 mockedName, _ := snap.SplitInstanceName(expectedInstanceName) 2947 2948 // setup done 2949 installQueue := []string{} 2950 unsafeReadSnapInfo = func(path string) (*snap.Info, error) { 2951 return &snap.Info{SuggestedName: mockedName}, nil 2952 } 2953 2954 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 2955 // NOTE: ubuntu-core is not installed in developer mode 2956 c.Check(flags, check.Equals, snapstate.Flags{}) 2957 installQueue = append(installQueue, name) 2958 2959 t := s.NewTask("fake-install-snap", "Doing a fake install") 2960 return state.NewTaskSet(t), nil 2961 } 2962 2963 snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) { 2964 c.Check(flags, check.DeepEquals, expectedFlags) 2965 2966 c.Check(path, testutil.FileEquals, "xyzzy") 2967 2968 c.Check(name, check.Equals, expectedInstanceName) 2969 2970 installQueue = append(installQueue, si.RealName+"::"+path) 2971 t := s.NewTask("fake-install-snap", "Doing a fake install") 2972 return state.NewTaskSet(t), &snap.Info{SuggestedName: name}, nil 2973 } 2974 2975 buf := bytes.NewBufferString(content) 2976 req, err := http.NewRequest("POST", "/v2/snaps", buf) 2977 c.Assert(err, check.IsNil) 2978 for k, v := range head { 2979 req.Header.Set(k, v) 2980 } 2981 2982 rsp := postSnaps(snapsCmd, req, nil).(*resp) 2983 c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) 2984 n := 1 2985 c.Assert(installQueue, check.HasLen, n) 2986 c.Check(installQueue[n-1], check.Matches, "local::.*/"+regexp.QuoteMeta(dirs.LocalInstallBlobTempPrefix)+".*") 2987 2988 st := d.overlord.State() 2989 st.Lock() 2990 defer st.Unlock() 2991 chg := st.Change(rsp.Change) 2992 c.Assert(chg, check.NotNil) 2993 2994 c.Check(soon, check.Equals, 1) 2995 2996 c.Assert(chg.Tasks(), check.HasLen, n) 2997 2998 st.Unlock() 2999 s.waitTrivialChange(c, chg) 3000 st.Lock() 3001 3002 c.Check(chg.Kind(), check.Equals, "install-snap") 3003 var names []string 3004 err = chg.Get("snap-names", &names) 3005 c.Assert(err, check.IsNil) 3006 c.Check(names, check.DeepEquals, []string{expectedInstanceName}) 3007 var apiData map[string]interface{} 3008 err = chg.Get("api-data", &apiData) 3009 c.Assert(err, check.IsNil) 3010 c.Check(apiData, check.DeepEquals, map[string]interface{}{ 3011 "snap-name": expectedInstanceName, 3012 }) 3013 3014 return chg.Summary() 3015 } 3016 3017 func (s *apiSuite) TestSideloadSnapJailModeAndDevmode(c *check.C) { 3018 body := "" + 3019 "----hello--\r\n" + 3020 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 3021 "\r\n" + 3022 "xyzzy\r\n" + 3023 "----hello--\r\n" + 3024 "Content-Disposition: form-data; name=\"jailmode\"\r\n" + 3025 "\r\n" + 3026 "true\r\n" + 3027 "----hello--\r\n" + 3028 "Content-Disposition: form-data; name=\"devmode\"\r\n" + 3029 "\r\n" + 3030 "true\r\n" + 3031 "----hello--\r\n" 3032 s.daemonWithOverlordMock(c) 3033 3034 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) 3035 c.Assert(err, check.IsNil) 3036 req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") 3037 3038 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3039 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3040 c.Check(rsp.Result.(*errorResult).Message, check.Equals, "cannot use devmode and jailmode flags together") 3041 } 3042 3043 func (s *apiSuite) TestSideloadSnapJailModeInDevModeOS(c *check.C) { 3044 body := "" + 3045 "----hello--\r\n" + 3046 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 3047 "\r\n" + 3048 "xyzzy\r\n" + 3049 "----hello--\r\n" + 3050 "Content-Disposition: form-data; name=\"jailmode\"\r\n" + 3051 "\r\n" + 3052 "true\r\n" + 3053 "----hello--\r\n" 3054 s.daemonWithOverlordMock(c) 3055 3056 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) 3057 c.Assert(err, check.IsNil) 3058 req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") 3059 3060 restore := sandbox.MockForceDevMode(true) 3061 defer restore() 3062 3063 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3064 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3065 c.Check(rsp.Result.(*errorResult).Message, check.Equals, "this system cannot honour the jailmode flag") 3066 } 3067 3068 func (s *apiSuite) TestLocalInstallSnapDeriveSideInfo(c *check.C) { 3069 d := s.daemonWithOverlordMock(c) 3070 // add the assertions first 3071 st := d.overlord.State() 3072 3073 dev1Acct := assertstest.NewAccount(s.storeSigning, "devel1", nil, "") 3074 3075 snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ 3076 "series": "16", 3077 "snap-id": "x-id", 3078 "snap-name": "x", 3079 "publisher-id": dev1Acct.AccountID(), 3080 "timestamp": time.Now().Format(time.RFC3339), 3081 }, nil, "") 3082 c.Assert(err, check.IsNil) 3083 3084 snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ 3085 "snap-sha3-384": "YK0GWATaZf09g_fvspYPqm_qtaiqf-KjaNj5uMEQCjQpuXWPjqQbeBINL5H_A0Lo", 3086 "snap-size": "5", 3087 "snap-id": "x-id", 3088 "snap-revision": "41", 3089 "developer-id": dev1Acct.AccountID(), 3090 "timestamp": time.Now().Format(time.RFC3339), 3091 }, nil, "") 3092 c.Assert(err, check.IsNil) 3093 3094 func() { 3095 st.Lock() 3096 defer st.Unlock() 3097 assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""), dev1Acct, snapDecl, snapRev) 3098 }() 3099 3100 body := "" + 3101 "----hello--\r\n" + 3102 "Content-Disposition: form-data; name=\"snap\"; filename=\"x.snap\"\r\n" + 3103 "\r\n" + 3104 "xyzzy\r\n" + 3105 "----hello--\r\n" 3106 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) 3107 c.Assert(err, check.IsNil) 3108 req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") 3109 3110 snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) { 3111 c.Check(flags, check.Equals, snapstate.Flags{RemoveSnapPath: true}) 3112 c.Check(si, check.DeepEquals, &snap.SideInfo{ 3113 RealName: "x", 3114 SnapID: "x-id", 3115 Revision: snap.R(41), 3116 }) 3117 3118 return state.NewTaskSet(), &snap.Info{SuggestedName: "x"}, nil 3119 } 3120 3121 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3122 c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) 3123 3124 st.Lock() 3125 defer st.Unlock() 3126 chg := st.Change(rsp.Change) 3127 c.Assert(chg, check.NotNil) 3128 c.Check(chg.Summary(), check.Equals, `Install "x" snap from file "x.snap"`) 3129 var names []string 3130 err = chg.Get("snap-names", &names) 3131 c.Assert(err, check.IsNil) 3132 c.Check(names, check.DeepEquals, []string{"x"}) 3133 var apiData map[string]interface{} 3134 err = chg.Get("api-data", &apiData) 3135 c.Assert(err, check.IsNil) 3136 c.Check(apiData, check.DeepEquals, map[string]interface{}{ 3137 "snap-name": "x", 3138 }) 3139 } 3140 3141 func (s *apiSuite) TestSideloadSnapNoSignaturesDangerOff(c *check.C) { 3142 body := "" + 3143 "----hello--\r\n" + 3144 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 3145 "\r\n" + 3146 "xyzzy\r\n" + 3147 "----hello--\r\n" 3148 s.daemonWithOverlordMock(c) 3149 3150 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) 3151 c.Assert(err, check.IsNil) 3152 req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") 3153 3154 // this is the prefix used for tempfiles for sideloading 3155 glob := filepath.Join(os.TempDir(), "snapd-sideload-pkg-*") 3156 glbBefore, _ := filepath.Glob(glob) 3157 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3158 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3159 c.Check(rsp.Result.(*errorResult).Message, check.Equals, `cannot find signatures with metadata for snap "x"`) 3160 glbAfter, _ := filepath.Glob(glob) 3161 c.Check(len(glbBefore), check.Equals, len(glbAfter)) 3162 } 3163 3164 func (s *apiSuite) TestSideloadSnapNotValidFormFile(c *check.C) { 3165 newTestDaemon(c) 3166 3167 // try a multipart/form-data upload with missing "name" 3168 content := "" + 3169 "----hello--\r\n" + 3170 "Content-Disposition: form-data; filename=\"x\"\r\n" + 3171 "\r\n" + 3172 "xyzzy\r\n" + 3173 "----hello--\r\n" 3174 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 3175 3176 buf := bytes.NewBufferString(content) 3177 req, err := http.NewRequest("POST", "/v2/snaps", buf) 3178 c.Assert(err, check.IsNil) 3179 for k, v := range head { 3180 req.Header.Set(k, v) 3181 } 3182 3183 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3184 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3185 c.Assert(rsp.Result.(*errorResult).Message, check.Matches, `cannot find "snap" file field in provided multipart/form-data payload`) 3186 } 3187 3188 func (s *apiSuite) TestSideloadSnapChangeConflict(c *check.C) { 3189 body := "" + 3190 "----hello--\r\n" + 3191 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 3192 "\r\n" + 3193 "xyzzy\r\n" + 3194 "----hello--\r\n" + 3195 "Content-Disposition: form-data; name=\"dangerous\"\r\n" + 3196 "\r\n" + 3197 "true\r\n" + 3198 "----hello--\r\n" 3199 s.daemonWithOverlordMock(c) 3200 3201 unsafeReadSnapInfo = func(path string) (*snap.Info, error) { 3202 return &snap.Info{SuggestedName: "foo"}, nil 3203 } 3204 3205 snapstateInstallPath = func(s *state.State, si *snap.SideInfo, path, name, channel string, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) { 3206 return nil, nil, &snapstate.ChangeConflictError{Snap: "foo"} 3207 } 3208 3209 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) 3210 c.Assert(err, check.IsNil) 3211 req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") 3212 3213 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3214 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3215 c.Check(rsp.Result.(*errorResult).Kind, check.Equals, client.ErrorKindSnapChangeConflict) 3216 } 3217 3218 func (s *apiSuite) TestSideloadSnapInstanceName(c *check.C) { 3219 // try a multipart/form-data upload 3220 body := sideLoadBodyWithoutDevMode + 3221 "Content-Disposition: form-data; name=\"name\"\r\n" + 3222 "\r\n" + 3223 "local_instance\r\n" + 3224 "----hello--\r\n" 3225 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 3226 chgSummary := s.sideloadCheck(c, body, head, "local_instance", snapstate.Flags{RemoveSnapPath: true}) 3227 c.Check(chgSummary, check.Equals, `Install "local_instance" snap from file "a/b/local.snap"`) 3228 } 3229 3230 func (s *apiSuite) TestSideloadSnapInstanceNameNoKey(c *check.C) { 3231 // try a multipart/form-data upload 3232 body := sideLoadBodyWithoutDevMode + 3233 "Content-Disposition: form-data; name=\"name\"\r\n" + 3234 "\r\n" + 3235 "local\r\n" + 3236 "----hello--\r\n" 3237 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 3238 chgSummary := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true}) 3239 c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) 3240 } 3241 3242 func (s *apiSuite) TestSideloadSnapInstanceNameMismatch(c *check.C) { 3243 s.daemonWithFakeSnapManager(c) 3244 3245 unsafeReadSnapInfo = func(path string) (*snap.Info, error) { 3246 return &snap.Info{SuggestedName: "bar"}, nil 3247 } 3248 3249 body := sideLoadBodyWithoutDevMode + 3250 "Content-Disposition: form-data; name=\"name\"\r\n" + 3251 "\r\n" + 3252 "foo_instance\r\n" + 3253 "----hello--\r\n" 3254 3255 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) 3256 c.Assert(err, check.IsNil) 3257 req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") 3258 3259 rsp := postSnaps(snapsCmd, req, nil).(*resp) 3260 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3261 c.Check(rsp.Result.(*errorResult).Message, check.Equals, `instance name "foo_instance" does not match snap name "bar"`) 3262 } 3263 3264 func (s *apiSuite) TestTrySnap(c *check.C) { 3265 d := s.daemonWithFakeSnapManager(c) 3266 3267 var err error 3268 3269 // mock a try dir 3270 tryDir := c.MkDir() 3271 snapYaml := filepath.Join(tryDir, "meta", "snap.yaml") 3272 err = os.MkdirAll(filepath.Dir(snapYaml), 0755) 3273 c.Assert(err, check.IsNil) 3274 err = ioutil.WriteFile(snapYaml, []byte("name: foo\nversion: 1.0\n"), 0644) 3275 c.Assert(err, check.IsNil) 3276 3277 reqForFlags := func(f snapstate.Flags) *http.Request { 3278 b := "" + 3279 "--hello\r\n" + 3280 "Content-Disposition: form-data; name=\"action\"\r\n" + 3281 "\r\n" + 3282 "try\r\n" + 3283 "--hello\r\n" + 3284 "Content-Disposition: form-data; name=\"snap-path\"\r\n" + 3285 "\r\n" + 3286 tryDir + "\r\n" + 3287 "--hello" 3288 3289 snip := "\r\n" + 3290 "Content-Disposition: form-data; name=%q\r\n" + 3291 "\r\n" + 3292 "true\r\n" + 3293 "--hello" 3294 3295 if f.DevMode { 3296 b += fmt.Sprintf(snip, "devmode") 3297 } 3298 if f.JailMode { 3299 b += fmt.Sprintf(snip, "jailmode") 3300 } 3301 if f.Classic { 3302 b += fmt.Sprintf(snip, "classic") 3303 } 3304 b += "--\r\n" 3305 3306 req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(b)) 3307 c.Assert(err, check.IsNil) 3308 req.Header.Set("Content-Type", "multipart/thing; boundary=hello") 3309 3310 return req 3311 } 3312 3313 st := d.overlord.State() 3314 st.Lock() 3315 defer st.Unlock() 3316 3317 for _, t := range []struct { 3318 flags snapstate.Flags 3319 desc string 3320 }{ 3321 {snapstate.Flags{}, "core; -"}, 3322 {snapstate.Flags{DevMode: true}, "core; devmode"}, 3323 {snapstate.Flags{JailMode: true}, "core; jailmode"}, 3324 {snapstate.Flags{Classic: true}, "core; classic"}, 3325 } { 3326 soon := 0 3327 ensureStateSoon = func(st *state.State) { 3328 soon++ 3329 ensureStateSoonImpl(st) 3330 } 3331 3332 tryWasCalled := true 3333 snapstateTryPath = func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) { 3334 c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc)) 3335 tryWasCalled = true 3336 t := s.NewTask("fake-install-snap", "Doing a fake try") 3337 return state.NewTaskSet(t), nil 3338 } 3339 3340 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 3341 if name != "core" { 3342 c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc)) 3343 } 3344 t := s.NewTask("fake-install-snap", "Doing a fake install") 3345 return state.NewTaskSet(t), nil 3346 } 3347 3348 // try the snap (without an installed core) 3349 st.Unlock() 3350 rsp := postSnaps(snapsCmd, reqForFlags(t.flags), nil).(*resp) 3351 st.Lock() 3352 c.Assert(rsp.Type, check.Equals, ResponseTypeAsync, check.Commentf(t.desc)) 3353 c.Assert(tryWasCalled, check.Equals, true, check.Commentf(t.desc)) 3354 3355 chg := st.Change(rsp.Change) 3356 c.Assert(chg, check.NotNil, check.Commentf(t.desc)) 3357 3358 c.Assert(chg.Tasks(), check.HasLen, 1, check.Commentf(t.desc)) 3359 3360 st.Unlock() 3361 s.waitTrivialChange(c, chg) 3362 st.Lock() 3363 3364 c.Check(chg.Kind(), check.Equals, "try-snap", check.Commentf(t.desc)) 3365 c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Try "%s" snap from %s`, "foo", tryDir), check.Commentf(t.desc)) 3366 var names []string 3367 err = chg.Get("snap-names", &names) 3368 c.Assert(err, check.IsNil, check.Commentf(t.desc)) 3369 c.Check(names, check.DeepEquals, []string{"foo"}, check.Commentf(t.desc)) 3370 var apiData map[string]interface{} 3371 err = chg.Get("api-data", &apiData) 3372 c.Assert(err, check.IsNil, check.Commentf(t.desc)) 3373 c.Check(apiData, check.DeepEquals, map[string]interface{}{ 3374 "snap-name": "foo", 3375 }, check.Commentf(t.desc)) 3376 3377 c.Check(soon, check.Equals, 1, check.Commentf(t.desc)) 3378 } 3379 } 3380 3381 func (s *apiSuite) TestTrySnapRelative(c *check.C) { 3382 req, err := http.NewRequest("POST", "/v2/snaps", nil) 3383 c.Assert(err, check.IsNil) 3384 3385 rsp := trySnap(snapsCmd, req, nil, "relative-path", snapstate.Flags{}).(*resp) 3386 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3387 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "need an absolute path") 3388 } 3389 3390 func (s *apiSuite) TestTrySnapNotDir(c *check.C) { 3391 req, err := http.NewRequest("POST", "/v2/snaps", nil) 3392 c.Assert(err, check.IsNil) 3393 3394 rsp := trySnap(snapsCmd, req, nil, "/does/not/exist", snapstate.Flags{}).(*resp) 3395 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3396 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "not a snap directory") 3397 } 3398 3399 func (s *apiSuite) TestTryChangeConflict(c *check.C) { 3400 s.daemonWithOverlordMock(c) 3401 3402 // mock a try dir 3403 tryDir := c.MkDir() 3404 3405 unsafeReadSnapInfo = func(path string) (*snap.Info, error) { 3406 return &snap.Info{SuggestedName: "foo"}, nil 3407 } 3408 3409 snapstateTryPath = func(s *state.State, name, path string, flags snapstate.Flags) (*state.TaskSet, error) { 3410 return nil, &snapstate.ChangeConflictError{Snap: "foo"} 3411 } 3412 3413 req, err := http.NewRequest("POST", "/v2/snaps", nil) 3414 c.Assert(err, check.IsNil) 3415 3416 rsp := trySnap(snapsCmd, req, nil, tryDir, snapstate.Flags{}).(*resp) 3417 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 3418 c.Check(rsp.Result.(*errorResult).Kind, check.Equals, client.ErrorKindSnapChangeConflict) 3419 } 3420 3421 func (s *apiSuite) runGetConf(c *check.C, snapName string, keys []string, statusCode int) map[string]interface{} { 3422 s.vars = map[string]string{"name": snapName} 3423 req, err := http.NewRequest("GET", "/v2/snaps/"+snapName+"/conf?keys="+strings.Join(keys, ","), nil) 3424 c.Check(err, check.IsNil) 3425 rec := httptest.NewRecorder() 3426 snapConfCmd.GET(snapConfCmd, req, nil).ServeHTTP(rec, req) 3427 c.Check(rec.Code, check.Equals, statusCode) 3428 3429 var body map[string]interface{} 3430 err = json.Unmarshal(rec.Body.Bytes(), &body) 3431 c.Check(err, check.IsNil) 3432 return body["result"].(map[string]interface{}) 3433 } 3434 3435 func (s *apiSuite) TestGetConfSingleKey(c *check.C) { 3436 d := s.daemon(c) 3437 3438 // Set a config that we'll get in a moment 3439 d.overlord.State().Lock() 3440 tr := config.NewTransaction(d.overlord.State()) 3441 tr.Set("test-snap", "test-key1", "test-value1") 3442 tr.Set("test-snap", "test-key2", "test-value2") 3443 tr.Commit() 3444 d.overlord.State().Unlock() 3445 3446 result := s.runGetConf(c, "test-snap", []string{"test-key1"}, 200) 3447 c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) 3448 3449 result = s.runGetConf(c, "test-snap", []string{"test-key1", "test-key2"}, 200) 3450 c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) 3451 } 3452 3453 func (s *apiSuite) TestGetConfCoreSystemAlias(c *check.C) { 3454 d := s.daemon(c) 3455 3456 // Set a config that we'll get in a moment 3457 d.overlord.State().Lock() 3458 tr := config.NewTransaction(d.overlord.State()) 3459 tr.Set("core", "test-key1", "test-value1") 3460 tr.Commit() 3461 d.overlord.State().Unlock() 3462 3463 result := s.runGetConf(c, "core", []string{"test-key1"}, 200) 3464 c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) 3465 3466 result = s.runGetConf(c, "system", []string{"test-key1"}, 200) 3467 c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1"}) 3468 } 3469 3470 func (s *apiSuite) TestGetConfMissingKey(c *check.C) { 3471 result := s.runGetConf(c, "test-snap", []string{"test-key2"}, 400) 3472 c.Check(result, check.DeepEquals, map[string]interface{}{ 3473 "value": map[string]interface{}{ 3474 "SnapName": "test-snap", 3475 "Key": "test-key2", 3476 }, 3477 "message": `snap "test-snap" has no "test-key2" configuration option`, 3478 "kind": "option-not-found", 3479 }) 3480 } 3481 3482 func (s *apiSuite) TestGetRootDocument(c *check.C) { 3483 d := s.daemon(c) 3484 d.overlord.State().Lock() 3485 tr := config.NewTransaction(d.overlord.State()) 3486 tr.Set("test-snap", "test-key1", "test-value1") 3487 tr.Set("test-snap", "test-key2", "test-value2") 3488 tr.Commit() 3489 d.overlord.State().Unlock() 3490 3491 result := s.runGetConf(c, "test-snap", nil, 200) 3492 c.Check(result, check.DeepEquals, map[string]interface{}{"test-key1": "test-value1", "test-key2": "test-value2"}) 3493 } 3494 3495 func (s *apiSuite) TestGetConfBadKey(c *check.C) { 3496 s.daemon(c) 3497 // TODO: this one in particular should really be a 400 also 3498 result := s.runGetConf(c, "test-snap", []string{"."}, 500) 3499 c.Check(result, check.DeepEquals, map[string]interface{}{"message": `invalid option name: ""`}) 3500 } 3501 3502 func (s *apiSuite) TestSetConf(c *check.C) { 3503 d := s.daemon(c) 3504 s.mockSnap(c, configYaml) 3505 3506 // Mock the hook runner 3507 hookRunner := testutil.MockCommand(c, "snap", "") 3508 defer hookRunner.Restore() 3509 3510 d.overlord.Loop() 3511 defer d.overlord.Stop() 3512 3513 text, err := json.Marshal(map[string]interface{}{"key": "value"}) 3514 c.Assert(err, check.IsNil) 3515 3516 buffer := bytes.NewBuffer(text) 3517 req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) 3518 c.Assert(err, check.IsNil) 3519 3520 s.vars = map[string]string{"name": "config-snap"} 3521 3522 rec := httptest.NewRecorder() 3523 snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) 3524 c.Check(rec.Code, check.Equals, 202) 3525 3526 var body map[string]interface{} 3527 err = json.Unmarshal(rec.Body.Bytes(), &body) 3528 c.Assert(err, check.IsNil) 3529 id := body["change"].(string) 3530 3531 st := d.overlord.State() 3532 st.Lock() 3533 chg := st.Change(id) 3534 st.Unlock() 3535 c.Assert(chg, check.NotNil) 3536 3537 <-chg.Ready() 3538 3539 st.Lock() 3540 err = chg.Err() 3541 st.Unlock() 3542 c.Assert(err, check.IsNil) 3543 3544 // Check that the configure hook was run correctly 3545 c.Check(hookRunner.Calls(), check.DeepEquals, [][]string{{ 3546 "snap", "run", "--hook", "configure", "-r", "unset", "config-snap", 3547 }}) 3548 } 3549 3550 func (s *apiSuite) TestSetConfCoreSystemAlias(c *check.C) { 3551 d := s.daemon(c) 3552 s.mockSnap(c, ` 3553 name: core 3554 version: 1 3555 `) 3556 // Mock the hook runner 3557 hookRunner := testutil.MockCommand(c, "snap", "") 3558 defer hookRunner.Restore() 3559 3560 d.overlord.Loop() 3561 defer d.overlord.Stop() 3562 3563 text, err := json.Marshal(map[string]interface{}{"proxy.ftp": "value"}) 3564 c.Assert(err, check.IsNil) 3565 3566 buffer := bytes.NewBuffer(text) 3567 req, err := http.NewRequest("PUT", "/v2/snaps/system/conf", buffer) 3568 c.Assert(err, check.IsNil) 3569 3570 s.vars = map[string]string{"name": "system"} 3571 3572 rec := httptest.NewRecorder() 3573 snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) 3574 c.Check(rec.Code, check.Equals, 202) 3575 3576 var body map[string]interface{} 3577 err = json.Unmarshal(rec.Body.Bytes(), &body) 3578 c.Assert(err, check.IsNil) 3579 id := body["change"].(string) 3580 3581 st := d.overlord.State() 3582 st.Lock() 3583 chg := st.Change(id) 3584 st.Unlock() 3585 c.Assert(chg, check.NotNil) 3586 3587 <-chg.Ready() 3588 3589 st.Lock() 3590 err = chg.Err() 3591 c.Assert(err, check.IsNil) 3592 3593 tr := config.NewTransaction(st) 3594 st.Unlock() 3595 c.Assert(err, check.IsNil) 3596 3597 var value string 3598 tr.Get("core", "proxy.ftp", &value) 3599 c.Assert(value, check.Equals, "value") 3600 3601 } 3602 3603 func (s *apiSuite) TestSetConfNumber(c *check.C) { 3604 d := s.daemon(c) 3605 s.mockSnap(c, configYaml) 3606 3607 // Mock the hook runner 3608 hookRunner := testutil.MockCommand(c, "snap", "") 3609 defer hookRunner.Restore() 3610 3611 d.overlord.Loop() 3612 defer d.overlord.Stop() 3613 3614 text, err := json.Marshal(map[string]interface{}{"key": 1234567890}) 3615 c.Assert(err, check.IsNil) 3616 3617 buffer := bytes.NewBuffer(text) 3618 req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) 3619 c.Assert(err, check.IsNil) 3620 3621 s.vars = map[string]string{"name": "config-snap"} 3622 3623 rec := httptest.NewRecorder() 3624 snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) 3625 c.Check(rec.Code, check.Equals, 202) 3626 3627 var body map[string]interface{} 3628 err = json.Unmarshal(rec.Body.Bytes(), &body) 3629 c.Assert(err, check.IsNil) 3630 id := body["change"].(string) 3631 3632 st := d.overlord.State() 3633 st.Lock() 3634 chg := st.Change(id) 3635 st.Unlock() 3636 c.Assert(chg, check.NotNil) 3637 3638 <-chg.Ready() 3639 3640 st.Lock() 3641 defer st.Unlock() 3642 tr := config.NewTransaction(d.overlord.State()) 3643 var result interface{} 3644 c.Assert(tr.Get("config-snap", "key", &result), check.IsNil) 3645 c.Assert(result, check.DeepEquals, json.Number("1234567890")) 3646 } 3647 3648 func (s *apiSuite) TestSetConfBadSnap(c *check.C) { 3649 s.daemonWithOverlordMock(c) 3650 3651 text, err := json.Marshal(map[string]interface{}{"key": "value"}) 3652 c.Assert(err, check.IsNil) 3653 3654 buffer := bytes.NewBuffer(text) 3655 req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) 3656 c.Assert(err, check.IsNil) 3657 3658 s.vars = map[string]string{"name": "config-snap"} 3659 3660 rec := httptest.NewRecorder() 3661 snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) 3662 c.Check(rec.Code, check.Equals, 404) 3663 3664 var body map[string]interface{} 3665 err = json.Unmarshal(rec.Body.Bytes(), &body) 3666 c.Assert(err, check.IsNil) 3667 c.Check(body, check.DeepEquals, map[string]interface{}{ 3668 "status-code": 404., 3669 "status": "Not Found", 3670 "result": map[string]interface{}{ 3671 "message": `snap "config-snap" is not installed`, 3672 "kind": "snap-not-found", 3673 "value": "config-snap", 3674 }, 3675 "type": "error"}) 3676 } 3677 3678 func simulateConflict(o *overlord.Overlord, name string) { 3679 st := o.State() 3680 st.Lock() 3681 defer st.Unlock() 3682 t := st.NewTask("link-snap", "...") 3683 snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{ 3684 RealName: name, 3685 }} 3686 t.Set("snap-setup", snapsup) 3687 chg := st.NewChange("manip", "...") 3688 chg.AddTask(t) 3689 } 3690 3691 func (s *apiSuite) TestSetConfChangeConflict(c *check.C) { 3692 d := s.daemon(c) 3693 s.mockSnap(c, configYaml) 3694 3695 simulateConflict(d.overlord, "config-snap") 3696 3697 text, err := json.Marshal(map[string]interface{}{"key": "value"}) 3698 c.Assert(err, check.IsNil) 3699 3700 buffer := bytes.NewBuffer(text) 3701 req, err := http.NewRequest("PUT", "/v2/snaps/config-snap/conf", buffer) 3702 c.Assert(err, check.IsNil) 3703 3704 s.vars = map[string]string{"name": "config-snap"} 3705 3706 rec := httptest.NewRecorder() 3707 snapConfCmd.PUT(snapConfCmd, req, nil).ServeHTTP(rec, req) 3708 c.Check(rec.Code, check.Equals, 409) 3709 3710 var body map[string]interface{} 3711 err = json.Unmarshal(rec.Body.Bytes(), &body) 3712 c.Assert(err, check.IsNil) 3713 c.Check(body, check.DeepEquals, map[string]interface{}{ 3714 "status-code": 409., 3715 "status": "Conflict", 3716 "result": map[string]interface{}{ 3717 "message": `snap "config-snap" has "manip" change in progress`, 3718 "kind": "snap-change-conflict", 3719 "value": map[string]interface{}{ 3720 "change-kind": "manip", 3721 "snap-name": "config-snap", 3722 }, 3723 }, 3724 "type": "error"}) 3725 } 3726 3727 func (s *apiSuite) TestAppIconGet(c *check.C) { 3728 d := s.daemon(c) 3729 3730 // have an active foo in the system 3731 info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "") 3732 3733 // have an icon for it in the package itself 3734 iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick") 3735 c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil) 3736 c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil) 3737 3738 s.vars = map[string]string{"name": "foo"} 3739 req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) 3740 c.Assert(err, check.IsNil) 3741 3742 rec := httptest.NewRecorder() 3743 3744 appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) 3745 c.Check(rec.Code, check.Equals, 200) 3746 c.Check(rec.Body.String(), check.Equals, "ick") 3747 } 3748 3749 func (s *apiSuite) TestAppIconGetInactive(c *check.C) { 3750 d := s.daemon(c) 3751 3752 // have an *in*active foo in the system 3753 info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), false, "") 3754 3755 // have an icon for it in the package itself 3756 iconfile := filepath.Join(info.MountDir(), "meta", "gui", "icon.ick") 3757 c.Assert(os.MkdirAll(filepath.Dir(iconfile), 0755), check.IsNil) 3758 c.Check(ioutil.WriteFile(iconfile, []byte("ick"), 0644), check.IsNil) 3759 3760 s.vars = map[string]string{"name": "foo"} 3761 req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) 3762 c.Assert(err, check.IsNil) 3763 3764 rec := httptest.NewRecorder() 3765 3766 appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) 3767 c.Check(rec.Code, check.Equals, 200) 3768 c.Check(rec.Body.String(), check.Equals, "ick") 3769 } 3770 3771 func (s *apiSuite) TestAppIconGetNoIcon(c *check.C) { 3772 d := s.daemon(c) 3773 3774 // have an *in*active foo in the system 3775 info := s.mkInstalledInState(c, d, "foo", "bar", "v1", snap.R(10), true, "") 3776 3777 // NO ICON! 3778 err := os.RemoveAll(filepath.Join(info.MountDir(), "meta", "gui", "icon.svg")) 3779 c.Assert(err, check.IsNil) 3780 3781 s.vars = map[string]string{"name": "foo"} 3782 req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) 3783 c.Assert(err, check.IsNil) 3784 3785 rec := httptest.NewRecorder() 3786 3787 appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) 3788 c.Check(rec.Code/100, check.Equals, 4) 3789 } 3790 3791 func (s *apiSuite) TestAppIconGetNoApp(c *check.C) { 3792 s.daemon(c) 3793 3794 s.vars = map[string]string{"name": "foo"} 3795 req, err := http.NewRequest("GET", "/v2/icons/foo/icon", nil) 3796 c.Assert(err, check.IsNil) 3797 3798 rec := httptest.NewRecorder() 3799 3800 appIconCmd.GET(appIconCmd, req, nil).ServeHTTP(rec, req) 3801 c.Check(rec.Code, check.Equals, 404) 3802 } 3803 3804 func (s *apiSuite) TestNotInstalledSnapIcon(c *check.C) { 3805 info := &snap.Info{SuggestedName: "notInstalledSnap", Media: []snap.MediaInfo{{Type: "icon", URL: "icon.svg"}}} 3806 iconfile := snapIcon(info) 3807 c.Check(iconfile, check.Equals, "") 3808 } 3809 3810 func (s *apiSuite) TestInstallOnNonDevModeDistro(c *check.C) { 3811 s.testInstall(c, false, snapstate.Flags{}, snap.R(0)) 3812 } 3813 func (s *apiSuite) TestInstallOnDevModeDistro(c *check.C) { 3814 s.testInstall(c, true, snapstate.Flags{}, snap.R(0)) 3815 } 3816 func (s *apiSuite) TestInstallRevision(c *check.C) { 3817 s.testInstall(c, false, snapstate.Flags{}, snap.R(42)) 3818 } 3819 3820 func (s *apiSuite) testInstall(c *check.C, forcedDevmode bool, flags snapstate.Flags, revision snap.Revision) { 3821 calledFlags := snapstate.Flags{} 3822 installQueue := []string{} 3823 restore := sandbox.MockForceDevMode(forcedDevmode) 3824 defer restore() 3825 3826 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 3827 calledFlags = flags 3828 installQueue = append(installQueue, name) 3829 c.Check(revision, check.Equals, opts.Revision) 3830 3831 t := s.NewTask("fake-install-snap", "Doing a fake install") 3832 return state.NewTaskSet(t), nil 3833 } 3834 3835 defer func() { 3836 snapstateInstall = nil 3837 }() 3838 3839 d := s.daemonWithFakeSnapManager(c) 3840 3841 var buf bytes.Buffer 3842 if revision.Unset() { 3843 buf.WriteString(`{"action": "install"}`) 3844 } else { 3845 fmt.Fprintf(&buf, `{"action": "install", "revision": %s}`, revision.String()) 3846 } 3847 req, err := http.NewRequest("POST", "/v2/snaps/some-snap", &buf) 3848 c.Assert(err, check.IsNil) 3849 3850 s.vars = map[string]string{"name": "some-snap"} 3851 rsp := postSnap(snapCmd, req, nil).(*resp) 3852 3853 c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) 3854 3855 st := d.overlord.State() 3856 st.Lock() 3857 defer st.Unlock() 3858 chg := st.Change(rsp.Change) 3859 c.Assert(chg, check.NotNil) 3860 3861 c.Check(chg.Tasks(), check.HasLen, 1) 3862 3863 st.Unlock() 3864 s.waitTrivialChange(c, chg) 3865 st.Lock() 3866 3867 c.Check(chg.Status(), check.Equals, state.DoneStatus) 3868 c.Check(calledFlags, check.Equals, flags) 3869 c.Check(err, check.IsNil) 3870 c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) 3871 c.Check(chg.Kind(), check.Equals, "install-snap") 3872 c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap`) 3873 } 3874 3875 func (s *apiSuite) TestInstallUserAgentContextCreated(c *check.C) { 3876 snapstateInstall = func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 3877 s.ctx = ctx 3878 t := st.NewTask("fake-install-snap", "Doing a fake install") 3879 return state.NewTaskSet(t), nil 3880 } 3881 defer func() { 3882 snapstateInstall = nil 3883 }() 3884 3885 s.daemonWithFakeSnapManager(c) 3886 3887 var buf bytes.Buffer 3888 buf.WriteString(`{"action": "install"}`) 3889 req, err := http.NewRequest("POST", "/v2/snaps/some-snap", &buf) 3890 req.RemoteAddr = "pid=100;uid=0;socket=;" 3891 c.Assert(err, check.IsNil) 3892 req.Header.Add("User-Agent", "some-agent/1.0") 3893 3894 s.vars = map[string]string{"name": "some-snap"} 3895 rec := httptest.NewRecorder() 3896 snapCmd.ServeHTTP(rec, req) 3897 c.Assert(rec.Code, check.Equals, 202) 3898 c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0") 3899 } 3900 3901 func (s *apiSuite) TestRefresh(c *check.C) { 3902 var calledFlags snapstate.Flags 3903 calledUserID := 0 3904 installQueue := []string{} 3905 assertstateCalledUserID := 0 3906 3907 snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 3908 calledFlags = flags 3909 calledUserID = userID 3910 installQueue = append(installQueue, name) 3911 3912 t := s.NewTask("fake-refresh-snap", "Doing a fake install") 3913 return state.NewTaskSet(t), nil 3914 } 3915 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 3916 assertstateCalledUserID = userID 3917 return nil 3918 } 3919 3920 d := s.daemon(c) 3921 inst := &snapInstruction{ 3922 Action: "refresh", 3923 Snaps: []string{"some-snap"}, 3924 userID: 17, 3925 } 3926 3927 st := d.overlord.State() 3928 st.Lock() 3929 defer st.Unlock() 3930 summary, _, err := inst.dispatch()(inst, st) 3931 c.Check(err, check.IsNil) 3932 3933 c.Check(assertstateCalledUserID, check.Equals, 17) 3934 c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) 3935 c.Check(calledUserID, check.Equals, 17) 3936 c.Check(err, check.IsNil) 3937 c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) 3938 c.Check(summary, check.Equals, `Refresh "some-snap" snap`) 3939 } 3940 3941 func (s *apiSuite) TestRefreshDevMode(c *check.C) { 3942 var calledFlags snapstate.Flags 3943 calledUserID := 0 3944 installQueue := []string{} 3945 3946 snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 3947 calledFlags = flags 3948 calledUserID = userID 3949 installQueue = append(installQueue, name) 3950 3951 t := s.NewTask("fake-refresh-snap", "Doing a fake install") 3952 return state.NewTaskSet(t), nil 3953 } 3954 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 3955 return nil 3956 } 3957 3958 d := s.daemon(c) 3959 inst := &snapInstruction{ 3960 Action: "refresh", 3961 DevMode: true, 3962 Snaps: []string{"some-snap"}, 3963 userID: 17, 3964 } 3965 3966 st := d.overlord.State() 3967 st.Lock() 3968 defer st.Unlock() 3969 summary, _, err := inst.dispatch()(inst, st) 3970 c.Check(err, check.IsNil) 3971 3972 flags := snapstate.Flags{} 3973 flags.DevMode = true 3974 c.Check(calledFlags, check.DeepEquals, flags) 3975 c.Check(calledUserID, check.Equals, 17) 3976 c.Check(err, check.IsNil) 3977 c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) 3978 c.Check(summary, check.Equals, `Refresh "some-snap" snap`) 3979 } 3980 3981 func (s *apiSuite) TestRefreshClassic(c *check.C) { 3982 var calledFlags snapstate.Flags 3983 3984 snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 3985 calledFlags = flags 3986 return nil, nil 3987 } 3988 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 3989 return nil 3990 } 3991 3992 d := s.daemon(c) 3993 inst := &snapInstruction{ 3994 Action: "refresh", 3995 Classic: true, 3996 Snaps: []string{"some-snap"}, 3997 userID: 17, 3998 } 3999 4000 st := d.overlord.State() 4001 st.Lock() 4002 defer st.Unlock() 4003 _, _, err := inst.dispatch()(inst, st) 4004 c.Check(err, check.IsNil) 4005 4006 c.Check(calledFlags, check.DeepEquals, snapstate.Flags{Classic: true}) 4007 } 4008 4009 func (s *apiSuite) TestRefreshIgnoreValidation(c *check.C) { 4010 var calledFlags snapstate.Flags 4011 calledUserID := 0 4012 installQueue := []string{} 4013 4014 snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4015 calledFlags = flags 4016 calledUserID = userID 4017 installQueue = append(installQueue, name) 4018 4019 t := s.NewTask("fake-refresh-snap", "Doing a fake install") 4020 return state.NewTaskSet(t), nil 4021 } 4022 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4023 return nil 4024 } 4025 4026 d := s.daemon(c) 4027 inst := &snapInstruction{ 4028 Action: "refresh", 4029 IgnoreValidation: true, 4030 Snaps: []string{"some-snap"}, 4031 userID: 17, 4032 } 4033 4034 st := d.overlord.State() 4035 st.Lock() 4036 defer st.Unlock() 4037 summary, _, err := inst.dispatch()(inst, st) 4038 c.Check(err, check.IsNil) 4039 4040 flags := snapstate.Flags{} 4041 flags.IgnoreValidation = true 4042 4043 c.Check(calledFlags, check.DeepEquals, flags) 4044 c.Check(calledUserID, check.Equals, 17) 4045 c.Check(err, check.IsNil) 4046 c.Check(installQueue, check.DeepEquals, []string{"some-snap"}) 4047 c.Check(summary, check.Equals, `Refresh "some-snap" snap`) 4048 } 4049 4050 func (s *apiSuite) TestRefreshCohort(c *check.C) { 4051 cohort := "" 4052 4053 snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4054 cohort = opts.CohortKey 4055 4056 t := s.NewTask("fake-refresh-snap", "Doing a fake install") 4057 return state.NewTaskSet(t), nil 4058 } 4059 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4060 return nil 4061 } 4062 4063 d := s.daemon(c) 4064 inst := &snapInstruction{ 4065 Action: "refresh", 4066 Snaps: []string{"some-snap"}, 4067 snapRevisionOptions: snapRevisionOptions{ 4068 CohortKey: "xyzzy", 4069 }, 4070 } 4071 4072 st := d.overlord.State() 4073 st.Lock() 4074 defer st.Unlock() 4075 summary, _, err := inst.dispatch()(inst, st) 4076 c.Check(err, check.IsNil) 4077 4078 c.Check(cohort, check.Equals, "xyzzy") 4079 c.Check(summary, check.Equals, `Refresh "some-snap" snap`) 4080 } 4081 4082 func (s *apiSuite) TestRefreshLeaveCohort(c *check.C) { 4083 var leave *bool 4084 4085 snapstateUpdate = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4086 leave = &opts.LeaveCohort 4087 4088 t := s.NewTask("fake-refresh-snap", "Doing a fake install") 4089 return state.NewTaskSet(t), nil 4090 } 4091 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4092 return nil 4093 } 4094 4095 d := s.daemon(c) 4096 inst := &snapInstruction{ 4097 Action: "refresh", 4098 snapRevisionOptions: snapRevisionOptions{LeaveCohort: true}, 4099 Snaps: []string{"some-snap"}, 4100 } 4101 4102 st := d.overlord.State() 4103 st.Lock() 4104 defer st.Unlock() 4105 summary, _, err := inst.dispatch()(inst, st) 4106 c.Check(err, check.IsNil) 4107 4108 c.Check(*leave, check.Equals, true) 4109 c.Check(summary, check.Equals, `Refresh "some-snap" snap`) 4110 } 4111 4112 func (s *apiSuite) TestSwitchInstruction(c *check.C) { 4113 var cohort, channel string 4114 var leave *bool 4115 snapstateSwitch = func(s *state.State, name string, opts *snapstate.RevisionOptions) (*state.TaskSet, error) { 4116 cohort = opts.CohortKey 4117 leave = &opts.LeaveCohort 4118 channel = opts.Channel 4119 4120 t := s.NewTask("fake-switch", "Doing a fake switch") 4121 return state.NewTaskSet(t), nil 4122 } 4123 4124 d := s.daemon(c) 4125 st := d.overlord.State() 4126 4127 type T struct { 4128 channel string 4129 cohort string 4130 leave bool 4131 summary string 4132 } 4133 table := []T{ 4134 {"", "some-cohort", false, `Switch "some-snap" snap to cohort "…me-cohort"`}, 4135 {"some-channel", "", false, `Switch "some-snap" snap to channel "some-channel"`}, 4136 {"some-channel", "some-cohort", false, `Switch "some-snap" snap to channel "some-channel" and cohort "…me-cohort"`}, 4137 {"", "", true, `Switch "some-snap" snap away from cohort`}, 4138 {"some-channel", "", true, `Switch "some-snap" snap to channel "some-channel" and away from cohort`}, 4139 } 4140 4141 for _, t := range table { 4142 cohort, channel = "", "" 4143 leave = nil 4144 inst := &snapInstruction{ 4145 Action: "switch", 4146 snapRevisionOptions: snapRevisionOptions{ 4147 CohortKey: t.cohort, 4148 LeaveCohort: t.leave, 4149 Channel: t.channel, 4150 }, 4151 Snaps: []string{"some-snap"}, 4152 } 4153 4154 st.Lock() 4155 summary, _, err := inst.dispatch()(inst, st) 4156 st.Unlock() 4157 c.Check(err, check.IsNil) 4158 4159 c.Check(cohort, check.Equals, t.cohort) 4160 c.Check(channel, check.Equals, t.channel) 4161 c.Check(summary, check.Equals, t.summary) 4162 c.Check(*leave, check.Equals, t.leave) 4163 } 4164 } 4165 4166 func (s *apiSuite) TestPostSnapOp(c *check.C) { 4167 s.testPostSnapsOp(c, "application/json") 4168 } 4169 4170 func (s *apiSuite) TestPostSnapOpMoreComplexContentType(c *check.C) { 4171 s.testPostSnapsOp(c, "application/json; charset=utf-8") 4172 } 4173 4174 func (s *apiSuite) TestPostSnapOpInvalidCharset(c *check.C) { 4175 buf := bytes.NewBufferString(`{"action": "refresh"}`) 4176 req, err := http.NewRequest("POST", "/v2/snaps", buf) 4177 c.Assert(err, check.IsNil) 4178 req.Header.Set("Content-Type", "application/json; charset=iso-8859-1") 4179 4180 rsp := postSnaps(snapsCmd, req, nil).(*resp) 4181 c.Check(rsp.Status, check.Equals, 400) 4182 c.Check(rsp.Result.(*errorResult).Message, testutil.Contains, "unknown charset in content type") 4183 } 4184 4185 func (s *apiSuite) testPostSnapsOp(c *check.C, contentType string) { 4186 assertstateRefreshSnapDeclarations = func(*state.State, int) error { return nil } 4187 snapstateUpdateMany = func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { 4188 c.Check(names, check.HasLen, 0) 4189 t := s.NewTask("fake-refresh-all", "Refreshing everything") 4190 return []string{"fake1", "fake2"}, []*state.TaskSet{state.NewTaskSet(t)}, nil 4191 } 4192 4193 d := s.daemonWithOverlordMock(c) 4194 4195 buf := bytes.NewBufferString(`{"action": "refresh"}`) 4196 req, err := http.NewRequest("POST", "/v2/snaps", buf) 4197 c.Assert(err, check.IsNil) 4198 req.Header.Set("Content-Type", contentType) 4199 4200 rsp, ok := postSnaps(snapsCmd, req, nil).(*resp) 4201 c.Assert(ok, check.Equals, true) 4202 c.Check(rsp.Type, check.Equals, ResponseTypeAsync) 4203 4204 st := d.overlord.State() 4205 st.Lock() 4206 defer st.Unlock() 4207 chg := st.Change(rsp.Change) 4208 c.Check(chg.Summary(), check.Equals, `Refresh snaps "fake1", "fake2"`) 4209 var apiData map[string]interface{} 4210 c.Check(chg.Get("api-data", &apiData), check.IsNil) 4211 c.Check(apiData["snap-names"], check.DeepEquals, []interface{}{"fake1", "fake2"}) 4212 } 4213 4214 func (s *apiSuite) TestRefreshAll(c *check.C) { 4215 refreshSnapDecls := false 4216 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4217 refreshSnapDecls = true 4218 return assertstate.RefreshSnapDeclarations(s, userID) 4219 } 4220 d := s.daemon(c) 4221 4222 for _, tst := range []struct { 4223 snaps []string 4224 msg string 4225 }{ 4226 {nil, "Refresh all snaps: no updates"}, 4227 {[]string{"fake"}, `Refresh snap "fake"`}, 4228 {[]string{"fake1", "fake2"}, `Refresh snaps "fake1", "fake2"`}, 4229 } { 4230 refreshSnapDecls = false 4231 4232 snapstateUpdateMany = func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { 4233 c.Check(names, check.HasLen, 0) 4234 t := s.NewTask("fake-refresh-all", "Refreshing everything") 4235 return tst.snaps, []*state.TaskSet{state.NewTaskSet(t)}, nil 4236 } 4237 4238 inst := &snapInstruction{Action: "refresh"} 4239 st := d.overlord.State() 4240 st.Lock() 4241 res, err := snapUpdateMany(inst, st) 4242 st.Unlock() 4243 c.Assert(err, check.IsNil) 4244 c.Check(res.Summary, check.Equals, tst.msg) 4245 c.Check(refreshSnapDecls, check.Equals, true) 4246 } 4247 } 4248 4249 func (s *apiSuite) TestRefreshAllNoChanges(c *check.C) { 4250 refreshSnapDecls := false 4251 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4252 refreshSnapDecls = true 4253 return assertstate.RefreshSnapDeclarations(s, userID) 4254 } 4255 4256 snapstateUpdateMany = func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { 4257 c.Check(names, check.HasLen, 0) 4258 return nil, nil, nil 4259 } 4260 4261 d := s.daemon(c) 4262 inst := &snapInstruction{Action: "refresh"} 4263 st := d.overlord.State() 4264 st.Lock() 4265 res, err := snapUpdateMany(inst, st) 4266 st.Unlock() 4267 c.Assert(err, check.IsNil) 4268 c.Check(res.Summary, check.Equals, `Refresh all snaps: no updates`) 4269 c.Check(refreshSnapDecls, check.Equals, true) 4270 } 4271 4272 func (s *apiSuite) TestRefreshMany(c *check.C) { 4273 refreshSnapDecls := false 4274 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4275 refreshSnapDecls = true 4276 return nil 4277 } 4278 4279 snapstateUpdateMany = func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { 4280 c.Check(names, check.HasLen, 2) 4281 t := s.NewTask("fake-refresh-2", "Refreshing two") 4282 return names, []*state.TaskSet{state.NewTaskSet(t)}, nil 4283 } 4284 4285 d := s.daemon(c) 4286 inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo", "bar"}} 4287 st := d.overlord.State() 4288 st.Lock() 4289 res, err := snapUpdateMany(inst, st) 4290 st.Unlock() 4291 c.Assert(err, check.IsNil) 4292 c.Check(res.Summary, check.Equals, `Refresh snaps "foo", "bar"`) 4293 c.Check(res.Affected, check.DeepEquals, inst.Snaps) 4294 c.Check(refreshSnapDecls, check.Equals, true) 4295 } 4296 4297 func (s *apiSuite) TestRefreshMany1(c *check.C) { 4298 refreshSnapDecls := false 4299 assertstateRefreshSnapDeclarations = func(s *state.State, userID int) error { 4300 refreshSnapDecls = true 4301 return nil 4302 } 4303 4304 snapstateUpdateMany = func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { 4305 c.Check(names, check.HasLen, 1) 4306 t := s.NewTask("fake-refresh-1", "Refreshing one") 4307 return names, []*state.TaskSet{state.NewTaskSet(t)}, nil 4308 } 4309 4310 d := s.daemon(c) 4311 inst := &snapInstruction{Action: "refresh", Snaps: []string{"foo"}} 4312 st := d.overlord.State() 4313 st.Lock() 4314 res, err := snapUpdateMany(inst, st) 4315 st.Unlock() 4316 c.Assert(err, check.IsNil) 4317 c.Check(res.Summary, check.Equals, `Refresh snap "foo"`) 4318 c.Check(res.Affected, check.DeepEquals, inst.Snaps) 4319 c.Check(refreshSnapDecls, check.Equals, true) 4320 } 4321 4322 func (s *apiSuite) TestInstallMany(c *check.C) { 4323 snapstateInstallMany = func(s *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { 4324 c.Check(names, check.HasLen, 2) 4325 t := s.NewTask("fake-install-2", "Install two") 4326 return names, []*state.TaskSet{state.NewTaskSet(t)}, nil 4327 } 4328 4329 d := s.daemon(c) 4330 inst := &snapInstruction{Action: "install", Snaps: []string{"foo", "bar"}} 4331 st := d.overlord.State() 4332 st.Lock() 4333 res, err := snapInstallMany(inst, st) 4334 st.Unlock() 4335 c.Assert(err, check.IsNil) 4336 c.Check(res.Summary, check.Equals, `Install snaps "foo", "bar"`) 4337 c.Check(res.Affected, check.DeepEquals, inst.Snaps) 4338 } 4339 4340 func (s *apiSuite) TestInstallManyEmptyName(c *check.C) { 4341 snapstateInstallMany = func(_ *state.State, _ []string, _ int) ([]string, []*state.TaskSet, error) { 4342 return nil, nil, errors.New("should not be called") 4343 } 4344 d := s.daemon(c) 4345 inst := &snapInstruction{Action: "install", Snaps: []string{"", "bar"}} 4346 st := d.overlord.State() 4347 st.Lock() 4348 res, err := snapInstallMany(inst, st) 4349 st.Unlock() 4350 c.Assert(res, check.IsNil) 4351 c.Assert(err, check.ErrorMatches, "cannot install snap with empty name") 4352 } 4353 4354 func (s *apiSuite) TestRemoveMany(c *check.C) { 4355 snapstateRemoveMany = func(s *state.State, names []string) ([]string, []*state.TaskSet, error) { 4356 c.Check(names, check.HasLen, 2) 4357 t := s.NewTask("fake-remove-2", "Remove two") 4358 return names, []*state.TaskSet{state.NewTaskSet(t)}, nil 4359 } 4360 4361 d := s.daemon(c) 4362 inst := &snapInstruction{Action: "remove", Snaps: []string{"foo", "bar"}} 4363 st := d.overlord.State() 4364 st.Lock() 4365 res, err := snapRemoveMany(inst, st) 4366 st.Unlock() 4367 c.Assert(err, check.IsNil) 4368 c.Check(res.Summary, check.Equals, `Remove snaps "foo", "bar"`) 4369 c.Check(res.Affected, check.DeepEquals, inst.Snaps) 4370 } 4371 4372 func (s *apiSuite) TestInstallFails(c *check.C) { 4373 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4374 t := s.NewTask("fake-install-snap-error", "Install task") 4375 return state.NewTaskSet(t), nil 4376 } 4377 4378 d := s.daemonWithFakeSnapManager(c) 4379 s.vars = map[string]string{"name": "hello-world"} 4380 buf := bytes.NewBufferString(`{"action": "install"}`) 4381 req, err := http.NewRequest("POST", "/v2/snaps/hello-world", buf) 4382 c.Assert(err, check.IsNil) 4383 4384 rsp := postSnap(snapCmd, req, nil).(*resp) 4385 4386 c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) 4387 4388 st := d.overlord.State() 4389 st.Lock() 4390 defer st.Unlock() 4391 chg := st.Change(rsp.Change) 4392 c.Assert(chg, check.NotNil) 4393 4394 c.Check(chg.Tasks(), check.HasLen, 1) 4395 4396 st.Unlock() 4397 s.waitTrivialChange(c, chg) 4398 st.Lock() 4399 4400 c.Check(chg.Err(), check.ErrorMatches, `(?sm).*Install task \(fake-install-snap-error errored\)`) 4401 } 4402 4403 func (s *apiSuite) TestInstallLeaveOld(c *check.C) { 4404 c.Skip("temporarily dropped half-baked support while sorting out flag mess") 4405 var calledFlags snapstate.Flags 4406 4407 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4408 calledFlags = flags 4409 4410 t := s.NewTask("fake-install-snap", "Doing a fake install") 4411 return state.NewTaskSet(t), nil 4412 } 4413 4414 d := s.daemon(c) 4415 inst := &snapInstruction{ 4416 Action: "install", 4417 LeaveOld: true, 4418 } 4419 4420 st := d.overlord.State() 4421 st.Lock() 4422 defer st.Unlock() 4423 _, _, err := inst.dispatch()(inst, st) 4424 c.Assert(err, check.IsNil) 4425 4426 c.Check(calledFlags, check.DeepEquals, snapstate.Flags{}) 4427 c.Check(err, check.IsNil) 4428 } 4429 4430 func (s *apiSuite) TestInstall(c *check.C) { 4431 var calledName string 4432 4433 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4434 calledName = name 4435 4436 t := s.NewTask("fake-install-snap", "Doing a fake install") 4437 return state.NewTaskSet(t), nil 4438 } 4439 4440 d := s.daemon(c) 4441 inst := &snapInstruction{ 4442 Action: "install", 4443 // Install the snap in developer mode 4444 DevMode: true, 4445 Snaps: []string{"fake"}, 4446 } 4447 4448 st := d.overlord.State() 4449 st.Lock() 4450 defer st.Unlock() 4451 _, _, err := inst.dispatch()(inst, st) 4452 c.Check(err, check.IsNil) 4453 c.Check(calledName, check.Equals, "fake") 4454 } 4455 4456 func (s *apiSuite) TestInstallCohort(c *check.C) { 4457 var calledName string 4458 var calledCohort string 4459 4460 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4461 calledName = name 4462 calledCohort = opts.CohortKey 4463 4464 t := s.NewTask("fake-install-snap", "Doing a fake install") 4465 return state.NewTaskSet(t), nil 4466 } 4467 4468 d := s.daemon(c) 4469 inst := &snapInstruction{ 4470 Action: "install", 4471 snapRevisionOptions: snapRevisionOptions{ 4472 CohortKey: "To the legion of the lost ones, to the cohort of the damned.", 4473 }, 4474 Snaps: []string{"fake"}, 4475 } 4476 4477 st := d.overlord.State() 4478 st.Lock() 4479 defer st.Unlock() 4480 msg, _, err := inst.dispatch()(inst, st) 4481 c.Check(err, check.IsNil) 4482 c.Check(calledName, check.Equals, "fake") 4483 c.Check(calledCohort, check.Equals, "To the legion of the lost ones, to the cohort of the damned.") 4484 c.Check(msg, check.Equals, `Install "fake" snap from "…e damned." cohort`) 4485 } 4486 4487 func (s *apiSuite) TestInstallDevMode(c *check.C) { 4488 var calledFlags snapstate.Flags 4489 4490 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4491 calledFlags = flags 4492 4493 t := s.NewTask("fake-install-snap", "Doing a fake install") 4494 return state.NewTaskSet(t), nil 4495 } 4496 4497 d := s.daemon(c) 4498 inst := &snapInstruction{ 4499 Action: "install", 4500 // Install the snap in developer mode 4501 DevMode: true, 4502 Snaps: []string{"fake"}, 4503 } 4504 4505 st := d.overlord.State() 4506 st.Lock() 4507 defer st.Unlock() 4508 _, _, err := inst.dispatch()(inst, st) 4509 c.Check(err, check.IsNil) 4510 4511 c.Check(calledFlags.DevMode, check.Equals, true) 4512 } 4513 4514 func (s *apiSuite) TestInstallJailMode(c *check.C) { 4515 var calledFlags snapstate.Flags 4516 4517 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 4518 calledFlags = flags 4519 4520 t := s.NewTask("fake-install-snap", "Doing a fake install") 4521 return state.NewTaskSet(t), nil 4522 } 4523 4524 d := s.daemon(c) 4525 inst := &snapInstruction{ 4526 Action: "install", 4527 JailMode: true, 4528 Snaps: []string{"fake"}, 4529 } 4530 4531 st := d.overlord.State() 4532 st.Lock() 4533 defer st.Unlock() 4534 _, _, err := inst.dispatch()(inst, st) 4535 c.Check(err, check.IsNil) 4536 4537 c.Check(calledFlags.JailMode, check.Equals, true) 4538 } 4539 4540 func (s *apiSuite) TestInstallJailModeDevModeOS(c *check.C) { 4541 restore := sandbox.MockForceDevMode(true) 4542 defer restore() 4543 4544 d := s.daemon(c) 4545 inst := &snapInstruction{ 4546 Action: "install", 4547 JailMode: true, 4548 Snaps: []string{"foo"}, 4549 } 4550 4551 st := d.overlord.State() 4552 st.Lock() 4553 defer st.Unlock() 4554 _, _, err := inst.dispatch()(inst, st) 4555 c.Check(err, check.ErrorMatches, "this system cannot honour the jailmode flag") 4556 } 4557 4558 func (s *apiSuite) TestInstallEmptyName(c *check.C) { 4559 snapstateInstall = func(ctx context.Context, _ *state.State, _ string, _ *snapstate.RevisionOptions, _ int, _ snapstate.Flags) (*state.TaskSet, error) { 4560 return nil, errors.New("should not be called") 4561 } 4562 d := s.daemon(c) 4563 inst := &snapInstruction{ 4564 Action: "install", 4565 Snaps: []string{""}, 4566 } 4567 4568 st := d.overlord.State() 4569 st.Lock() 4570 defer st.Unlock() 4571 _, _, err := inst.dispatch()(inst, st) 4572 c.Check(err, check.ErrorMatches, "cannot install snap with empty name") 4573 } 4574 4575 func (s *apiSuite) TestInstallJailModeDevMode(c *check.C) { 4576 d := s.daemon(c) 4577 inst := &snapInstruction{ 4578 Action: "install", 4579 DevMode: true, 4580 JailMode: true, 4581 Snaps: []string{"foo"}, 4582 } 4583 4584 st := d.overlord.State() 4585 st.Lock() 4586 defer st.Unlock() 4587 _, _, err := inst.dispatch()(inst, st) 4588 c.Check(err, check.ErrorMatches, "cannot use devmode and jailmode flags together") 4589 } 4590 4591 func (s *apiSuite) testRevertSnap(inst *snapInstruction, c *check.C) { 4592 queue := []string{} 4593 4594 instFlags, err := inst.modeFlags() 4595 c.Assert(err, check.IsNil) 4596 4597 snapstateRevert = func(s *state.State, name string, flags snapstate.Flags) (*state.TaskSet, error) { 4598 c.Check(flags, check.Equals, instFlags) 4599 queue = append(queue, name) 4600 return nil, nil 4601 } 4602 snapstateRevertToRevision = func(s *state.State, name string, rev snap.Revision, flags snapstate.Flags) (*state.TaskSet, error) { 4603 c.Check(flags, check.Equals, instFlags) 4604 queue = append(queue, fmt.Sprintf("%s (%s)", name, rev)) 4605 return nil, nil 4606 } 4607 4608 d := s.daemon(c) 4609 inst.Action = "revert" 4610 inst.Snaps = []string{"some-snap"} 4611 4612 st := d.overlord.State() 4613 st.Lock() 4614 defer st.Unlock() 4615 summary, _, err := inst.dispatch()(inst, st) 4616 c.Check(err, check.IsNil) 4617 if inst.Revision.Unset() { 4618 c.Check(queue, check.DeepEquals, []string{inst.Snaps[0]}) 4619 } else { 4620 c.Check(queue, check.DeepEquals, []string{fmt.Sprintf("%s (%s)", inst.Snaps[0], inst.Revision)}) 4621 } 4622 c.Check(summary, check.Equals, `Revert "some-snap" snap`) 4623 } 4624 4625 func (s *apiSuite) TestRevertSnap(c *check.C) { 4626 s.testRevertSnap(&snapInstruction{}, c) 4627 } 4628 4629 func (s *apiSuite) TestRevertSnapDevMode(c *check.C) { 4630 s.testRevertSnap(&snapInstruction{DevMode: true}, c) 4631 } 4632 4633 func (s *apiSuite) TestRevertSnapJailMode(c *check.C) { 4634 s.testRevertSnap(&snapInstruction{JailMode: true}, c) 4635 } 4636 4637 func (s *apiSuite) TestRevertSnapClassic(c *check.C) { 4638 s.testRevertSnap(&snapInstruction{Classic: true}, c) 4639 } 4640 4641 func (s *apiSuite) TestRevertSnapToRevision(c *check.C) { 4642 s.testRevertSnap(&snapInstruction{snapRevisionOptions: snapRevisionOptions{Revision: snap.R(1)}}, c) 4643 } 4644 4645 func (s *apiSuite) TestRevertSnapToRevisionDevMode(c *check.C) { 4646 s.testRevertSnap(&snapInstruction{snapRevisionOptions: snapRevisionOptions{Revision: snap.R(1)}, DevMode: true}, c) 4647 } 4648 4649 func (s *apiSuite) TestRevertSnapToRevisionJailMode(c *check.C) { 4650 s.testRevertSnap(&snapInstruction{snapRevisionOptions: snapRevisionOptions{Revision: snap.R(1)}, JailMode: true}, c) 4651 } 4652 4653 func (s *apiSuite) TestRevertSnapToRevisionClassic(c *check.C) { 4654 s.testRevertSnap(&snapInstruction{snapRevisionOptions: snapRevisionOptions{Revision: snap.R(1)}, Classic: true}, c) 4655 } 4656 4657 func snapList(rawSnaps interface{}) []map[string]interface{} { 4658 snaps := make([]map[string]interface{}, len(rawSnaps.([]*json.RawMessage))) 4659 for i, raw := range rawSnaps.([]*json.RawMessage) { 4660 err := json.Unmarshal([]byte(*raw), &snaps[i]) 4661 if err != nil { 4662 panic(err) 4663 } 4664 } 4665 return snaps 4666 } 4667 4668 // inverseCaseMapper implements SnapMapper to use lower case internally and upper case externally. 4669 type inverseCaseMapper struct { 4670 ifacestate.IdentityMapper // Embed the identity mapper to reuse empty state mapping functions. 4671 } 4672 4673 func (m *inverseCaseMapper) RemapSnapFromRequest(snapName string) string { 4674 return strings.ToLower(snapName) 4675 } 4676 4677 func (m *inverseCaseMapper) RemapSnapToResponse(snapName string) string { 4678 return strings.ToUpper(snapName) 4679 } 4680 4681 func (m *inverseCaseMapper) SystemSnapName() string { 4682 return "core" 4683 } 4684 4685 // Tests for GET /v2/interfaces 4686 4687 func (s *apiSuite) TestInterfacesLegacy(c *check.C) { 4688 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 4689 defer restore() 4690 // Install an inverse case mapper to exercise the interface mapping at the same time. 4691 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 4692 defer restore() 4693 4694 d := s.daemon(c) 4695 4696 var anotherConsumerYaml = ` 4697 name: another-consumer-%s 4698 version: 1 4699 apps: 4700 app: 4701 plugs: 4702 plug: 4703 interface: test 4704 key: value 4705 label: label 4706 ` 4707 s.mockSnap(c, consumerYaml) 4708 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def")) 4709 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc")) 4710 s.mockSnap(c, producerYaml) 4711 4712 repo := d.overlord.InterfaceManager().Repository() 4713 connRef := &interfaces.ConnRef{ 4714 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 4715 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 4716 } 4717 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 4718 c.Assert(err, check.IsNil) 4719 4720 st := s.d.overlord.State() 4721 st.Lock() 4722 st.Set("conns", map[string]interface{}{ 4723 "consumer:plug producer:slot": map[string]interface{}{ 4724 "interface": "test", 4725 "auto": true, 4726 }, 4727 "another-consumer-def:plug producer:slot": map[string]interface{}{ 4728 "interface": "test", 4729 "by-gadget": true, 4730 "auto": true, 4731 }, 4732 "another-consumer-abc:plug producer:slot": map[string]interface{}{ 4733 "interface": "test", 4734 "by-gadget": true, 4735 "auto": true, 4736 }, 4737 }) 4738 st.Unlock() 4739 4740 req, err := http.NewRequest("GET", "/v2/interfaces", nil) 4741 c.Assert(err, check.IsNil) 4742 rec := httptest.NewRecorder() 4743 interfacesCmd.GET(interfacesCmd, req, nil).ServeHTTP(rec, req) 4744 c.Check(rec.Code, check.Equals, 200) 4745 var body map[string]interface{} 4746 err = json.Unmarshal(rec.Body.Bytes(), &body) 4747 c.Check(err, check.IsNil) 4748 c.Check(body, check.DeepEquals, map[string]interface{}{ 4749 "result": map[string]interface{}{ 4750 "plugs": []interface{}{ 4751 map[string]interface{}{ 4752 "snap": "another-consumer-abc", 4753 "plug": "plug", 4754 "interface": "test", 4755 "attrs": map[string]interface{}{"key": "value"}, 4756 "apps": []interface{}{"app"}, 4757 "label": "label", 4758 "connections": []interface{}{ 4759 map[string]interface{}{"snap": "producer", "slot": "slot"}, 4760 }, 4761 }, 4762 map[string]interface{}{ 4763 "snap": "another-consumer-def", 4764 "plug": "plug", 4765 "interface": "test", 4766 "attrs": map[string]interface{}{"key": "value"}, 4767 "apps": []interface{}{"app"}, 4768 "label": "label", 4769 "connections": []interface{}{ 4770 map[string]interface{}{"snap": "producer", "slot": "slot"}, 4771 }, 4772 }, 4773 map[string]interface{}{ 4774 "snap": "consumer", 4775 "plug": "plug", 4776 "interface": "test", 4777 "attrs": map[string]interface{}{"key": "value"}, 4778 "apps": []interface{}{"app"}, 4779 "label": "label", 4780 "connections": []interface{}{ 4781 map[string]interface{}{"snap": "producer", "slot": "slot"}, 4782 }, 4783 }, 4784 }, 4785 "slots": []interface{}{ 4786 map[string]interface{}{ 4787 "snap": "producer", 4788 "slot": "slot", 4789 "interface": "test", 4790 "attrs": map[string]interface{}{"key": "value"}, 4791 "apps": []interface{}{"app"}, 4792 "label": "label", 4793 "connections": []interface{}{ 4794 map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"}, 4795 map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"}, 4796 map[string]interface{}{"snap": "consumer", "plug": "plug"}, 4797 }, 4798 }, 4799 }, 4800 }, 4801 "status": "OK", 4802 "status-code": 200.0, 4803 "type": "sync", 4804 }) 4805 } 4806 4807 func (s *apiSuite) TestInterfacesModern(c *check.C) { 4808 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 4809 defer restore() 4810 // Install an inverse case mapper to exercise the interface mapping at the same time. 4811 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 4812 defer restore() 4813 4814 d := s.daemon(c) 4815 4816 s.mockSnap(c, consumerYaml) 4817 s.mockSnap(c, producerYaml) 4818 4819 repo := d.overlord.InterfaceManager().Repository() 4820 connRef := &interfaces.ConnRef{ 4821 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 4822 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 4823 } 4824 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 4825 c.Assert(err, check.IsNil) 4826 4827 req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil) 4828 c.Assert(err, check.IsNil) 4829 rec := httptest.NewRecorder() 4830 interfacesCmd.GET(interfacesCmd, req, nil).ServeHTTP(rec, req) 4831 c.Check(rec.Code, check.Equals, 200) 4832 var body map[string]interface{} 4833 err = json.Unmarshal(rec.Body.Bytes(), &body) 4834 c.Check(err, check.IsNil) 4835 c.Check(body, check.DeepEquals, map[string]interface{}{ 4836 "result": []interface{}{ 4837 map[string]interface{}{ 4838 "name": "test", 4839 "plugs": []interface{}{ 4840 map[string]interface{}{ 4841 "snap": "consumer", 4842 "plug": "plug", 4843 "label": "label", 4844 "attrs": map[string]interface{}{ 4845 "key": "value", 4846 }, 4847 }}, 4848 "slots": []interface{}{ 4849 map[string]interface{}{ 4850 "snap": "producer", 4851 "slot": "slot", 4852 "label": "label", 4853 "attrs": map[string]interface{}{ 4854 "key": "value", 4855 }, 4856 }, 4857 }, 4858 }, 4859 }, 4860 "status": "OK", 4861 "status-code": 200.0, 4862 "type": "sync", 4863 }) 4864 } 4865 4866 // Test for POST /v2/interfaces 4867 4868 func (s *apiSuite) TestConnectPlugSuccess(c *check.C) { 4869 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 4870 defer restore() 4871 // Install an inverse case mapper to exercise the interface mapping at the same time. 4872 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 4873 defer restore() 4874 4875 d := s.daemon(c) 4876 4877 s.mockSnap(c, consumerYaml) 4878 s.mockSnap(c, producerYaml) 4879 4880 d.overlord.Loop() 4881 defer d.overlord.Stop() 4882 4883 action := &interfaceAction{ 4884 Action: "connect", 4885 Plugs: []plugJSON{{Snap: "CONSUMER", Name: "plug"}}, 4886 Slots: []slotJSON{{Snap: "PRODUCER", Name: "slot"}}, 4887 } 4888 text, err := json.Marshal(action) 4889 c.Assert(err, check.IsNil) 4890 buf := bytes.NewBuffer(text) 4891 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 4892 c.Assert(err, check.IsNil) 4893 rec := httptest.NewRecorder() 4894 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 4895 c.Check(rec.Code, check.Equals, 202) 4896 var body map[string]interface{} 4897 err = json.Unmarshal(rec.Body.Bytes(), &body) 4898 c.Check(err, check.IsNil) 4899 id := body["change"].(string) 4900 4901 st := d.overlord.State() 4902 st.Lock() 4903 chg := st.Change(id) 4904 st.Unlock() 4905 c.Assert(chg, check.NotNil) 4906 4907 <-chg.Ready() 4908 4909 st.Lock() 4910 err = chg.Err() 4911 st.Unlock() 4912 c.Assert(err, check.IsNil) 4913 4914 repo := d.overlord.InterfaceManager().Repository() 4915 ifaces := repo.Interfaces() 4916 c.Assert(ifaces.Connections, check.HasLen, 1) 4917 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 4918 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 4919 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 4920 }}) 4921 } 4922 4923 func (s *apiSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { 4924 d := s.daemon(c) 4925 4926 s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) 4927 s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "different"}) 4928 s.mockSnap(c, consumerYaml) 4929 s.mockSnap(c, differentProducerYaml) 4930 4931 action := &interfaceAction{ 4932 Action: "connect", 4933 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 4934 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 4935 } 4936 text, err := json.Marshal(action) 4937 c.Assert(err, check.IsNil) 4938 buf := bytes.NewBuffer(text) 4939 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 4940 c.Assert(err, check.IsNil) 4941 rec := httptest.NewRecorder() 4942 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 4943 c.Check(rec.Code, check.Equals, 400) 4944 var body map[string]interface{} 4945 err = json.Unmarshal(rec.Body.Bytes(), &body) 4946 c.Check(err, check.IsNil) 4947 c.Check(body, check.DeepEquals, map[string]interface{}{ 4948 "result": map[string]interface{}{ 4949 "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)", 4950 }, 4951 "status": "Bad Request", 4952 "status-code": 400.0, 4953 "type": "error", 4954 }) 4955 repo := d.overlord.InterfaceManager().Repository() 4956 ifaces := repo.Interfaces() 4957 c.Assert(ifaces.Connections, check.HasLen, 0) 4958 } 4959 4960 func (s *apiSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { 4961 d := s.daemon(c) 4962 4963 s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) 4964 // there is no consumer, no plug defined 4965 s.mockSnap(c, producerYaml) 4966 s.mockSnap(c, consumerYaml) 4967 4968 action := &interfaceAction{ 4969 Action: "connect", 4970 Plugs: []plugJSON{{Snap: "consumer", Name: "missingplug"}}, 4971 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 4972 } 4973 text, err := json.Marshal(action) 4974 c.Assert(err, check.IsNil) 4975 buf := bytes.NewBuffer(text) 4976 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 4977 c.Assert(err, check.IsNil) 4978 rec := httptest.NewRecorder() 4979 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 4980 c.Check(rec.Code, check.Equals, 400) 4981 4982 var body map[string]interface{} 4983 err = json.Unmarshal(rec.Body.Bytes(), &body) 4984 c.Check(err, check.IsNil) 4985 c.Check(body, check.DeepEquals, map[string]interface{}{ 4986 "result": map[string]interface{}{ 4987 "message": "snap \"consumer\" has no plug named \"missingplug\"", 4988 }, 4989 "status": "Bad Request", 4990 "status-code": 400.0, 4991 "type": "error", 4992 }) 4993 4994 repo := d.overlord.InterfaceManager().Repository() 4995 ifaces := repo.Interfaces() 4996 c.Assert(ifaces.Connections, check.HasLen, 0) 4997 } 4998 4999 func (s *apiSuite) TestConnectAlreadyConnected(c *check.C) { 5000 d := s.daemon(c) 5001 5002 s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) 5003 // there is no consumer, no plug defined 5004 s.mockSnap(c, producerYaml) 5005 s.mockSnap(c, consumerYaml) 5006 5007 repo := d.overlord.InterfaceManager().Repository() 5008 connRef := &interfaces.ConnRef{ 5009 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 5010 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 5011 } 5012 5013 d.overlord.Loop() 5014 defer d.overlord.Stop() 5015 5016 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 5017 c.Assert(err, check.IsNil) 5018 conns := map[string]interface{}{ 5019 "consumer:plug producer:slot": map[string]interface{}{ 5020 "auto": false, 5021 }, 5022 } 5023 st := d.overlord.State() 5024 st.Lock() 5025 st.Set("conns", conns) 5026 st.Unlock() 5027 5028 action := &interfaceAction{ 5029 Action: "connect", 5030 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5031 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5032 } 5033 text, err := json.Marshal(action) 5034 c.Assert(err, check.IsNil) 5035 buf := bytes.NewBuffer(text) 5036 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5037 c.Assert(err, check.IsNil) 5038 rec := httptest.NewRecorder() 5039 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5040 c.Check(rec.Code, check.Equals, 202) 5041 var body map[string]interface{} 5042 err = json.Unmarshal(rec.Body.Bytes(), &body) 5043 c.Check(err, check.IsNil) 5044 id := body["change"].(string) 5045 5046 st.Lock() 5047 chg := st.Change(id) 5048 c.Assert(chg.Tasks(), check.HasLen, 0) 5049 c.Assert(chg.Status(), check.Equals, state.DoneStatus) 5050 st.Unlock() 5051 } 5052 5053 func (s *apiSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { 5054 d := s.daemon(c) 5055 5056 s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) 5057 s.mockSnap(c, consumerYaml) 5058 s.mockSnap(c, producerYaml) 5059 // there is no producer, no slot defined 5060 5061 action := &interfaceAction{ 5062 Action: "connect", 5063 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5064 Slots: []slotJSON{{Snap: "producer", Name: "missingslot"}}, 5065 } 5066 text, err := json.Marshal(action) 5067 c.Assert(err, check.IsNil) 5068 buf := bytes.NewBuffer(text) 5069 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5070 c.Assert(err, check.IsNil) 5071 rec := httptest.NewRecorder() 5072 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5073 c.Check(rec.Code, check.Equals, 400) 5074 5075 var body map[string]interface{} 5076 err = json.Unmarshal(rec.Body.Bytes(), &body) 5077 c.Check(err, check.IsNil) 5078 c.Check(body, check.DeepEquals, map[string]interface{}{ 5079 "result": map[string]interface{}{ 5080 "message": "snap \"producer\" has no slot named \"missingslot\"", 5081 }, 5082 "status": "Bad Request", 5083 "status-code": 400.0, 5084 "type": "error", 5085 }) 5086 5087 repo := d.overlord.InterfaceManager().Repository() 5088 ifaces := repo.Interfaces() 5089 c.Assert(ifaces.Connections, check.HasLen, 0) 5090 } 5091 5092 func (s *apiSuite) TestConnectPlugChangeConflict(c *check.C) { 5093 d := s.daemon(c) 5094 5095 s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"}) 5096 s.mockSnap(c, consumerYaml) 5097 s.mockSnap(c, producerYaml) 5098 // there is no producer, no slot defined 5099 5100 simulateConflict(d.overlord, "consumer") 5101 5102 action := &interfaceAction{ 5103 Action: "connect", 5104 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5105 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5106 } 5107 text, err := json.Marshal(action) 5108 c.Assert(err, check.IsNil) 5109 buf := bytes.NewBuffer(text) 5110 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5111 c.Assert(err, check.IsNil) 5112 rec := httptest.NewRecorder() 5113 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5114 c.Check(rec.Code, check.Equals, 409) 5115 5116 var body map[string]interface{} 5117 err = json.Unmarshal(rec.Body.Bytes(), &body) 5118 c.Check(err, check.IsNil) 5119 c.Check(body, check.DeepEquals, map[string]interface{}{ 5120 "status-code": 409., 5121 "status": "Conflict", 5122 "result": map[string]interface{}{ 5123 "message": `snap "consumer" has "manip" change in progress`, 5124 "kind": "snap-change-conflict", 5125 "value": map[string]interface{}{ 5126 "change-kind": "manip", 5127 "snap-name": "consumer", 5128 }, 5129 }, 5130 "type": "error"}) 5131 } 5132 5133 func (s *apiSuite) TestConnectCoreSystemAlias(c *check.C) { 5134 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5135 defer revert() 5136 d := s.daemon(c) 5137 5138 s.mockSnap(c, consumerYaml) 5139 s.mockSnap(c, coreProducerYaml) 5140 5141 d.overlord.Loop() 5142 defer d.overlord.Stop() 5143 5144 action := &interfaceAction{ 5145 Action: "connect", 5146 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5147 Slots: []slotJSON{{Snap: "system", Name: "slot"}}, 5148 } 5149 text, err := json.Marshal(action) 5150 c.Assert(err, check.IsNil) 5151 buf := bytes.NewBuffer(text) 5152 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5153 c.Assert(err, check.IsNil) 5154 rec := httptest.NewRecorder() 5155 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5156 c.Check(rec.Code, check.Equals, 202) 5157 var body map[string]interface{} 5158 err = json.Unmarshal(rec.Body.Bytes(), &body) 5159 c.Check(err, check.IsNil) 5160 id := body["change"].(string) 5161 5162 st := d.overlord.State() 5163 st.Lock() 5164 chg := st.Change(id) 5165 st.Unlock() 5166 c.Assert(chg, check.NotNil) 5167 5168 <-chg.Ready() 5169 5170 st.Lock() 5171 err = chg.Err() 5172 st.Unlock() 5173 c.Assert(err, check.IsNil) 5174 5175 repo := d.overlord.InterfaceManager().Repository() 5176 ifaces := repo.Interfaces() 5177 c.Assert(ifaces.Connections, check.HasLen, 1) 5178 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 5179 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 5180 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}}) 5181 } 5182 5183 func (s *apiSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) { 5184 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5185 defer restore() 5186 // Install an inverse case mapper to exercise the interface mapping at the same time. 5187 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 5188 defer restore() 5189 d := s.daemon(c) 5190 5191 s.mockSnap(c, consumerYaml) 5192 s.mockSnap(c, producerYaml) 5193 5194 repo := d.overlord.InterfaceManager().Repository() 5195 connRef := &interfaces.ConnRef{ 5196 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 5197 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 5198 } 5199 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 5200 c.Assert(err, check.IsNil) 5201 5202 st := d.overlord.State() 5203 st.Lock() 5204 st.Set("conns", map[string]interface{}{ 5205 "consumer:plug producer:slot": map[string]interface{}{ 5206 "interface": "test", 5207 }, 5208 }) 5209 st.Unlock() 5210 5211 d.overlord.Loop() 5212 defer d.overlord.Stop() 5213 5214 action := &interfaceAction{ 5215 Action: "disconnect", 5216 Plugs: []plugJSON{{Snap: plugSnap, Name: plugName}}, 5217 Slots: []slotJSON{{Snap: slotSnap, Name: slotName}}, 5218 } 5219 text, err := json.Marshal(action) 5220 c.Assert(err, check.IsNil) 5221 buf := bytes.NewBuffer(text) 5222 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5223 c.Assert(err, check.IsNil) 5224 rec := httptest.NewRecorder() 5225 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5226 c.Check(rec.Code, check.Equals, 202) 5227 var body map[string]interface{} 5228 err = json.Unmarshal(rec.Body.Bytes(), &body) 5229 c.Check(err, check.IsNil) 5230 id := body["change"].(string) 5231 5232 st.Lock() 5233 chg := st.Change(id) 5234 st.Unlock() 5235 c.Assert(chg, check.NotNil) 5236 5237 <-chg.Ready() 5238 5239 st.Lock() 5240 err = chg.Err() 5241 st.Unlock() 5242 c.Assert(err, check.IsNil) 5243 5244 ifaces := repo.Interfaces() 5245 c.Assert(ifaces.Connections, check.HasLen, 0) 5246 } 5247 5248 func (s *apiSuite) TestDisconnectPlugSuccess(c *check.C) { 5249 s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot") 5250 } 5251 5252 func (s *apiSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) { 5253 s.testDisconnect(c, "", "", "PRODUCER", "slot") 5254 } 5255 5256 func (s *apiSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) { 5257 s.testDisconnect(c, "CONSUMER", "plug", "", "") 5258 } 5259 5260 func (s *apiSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { 5261 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5262 defer revert() 5263 s.daemon(c) 5264 5265 // there is no consumer, no plug defined 5266 s.mockSnap(c, producerYaml) 5267 5268 action := &interfaceAction{ 5269 Action: "disconnect", 5270 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5271 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5272 } 5273 text, err := json.Marshal(action) 5274 c.Assert(err, check.IsNil) 5275 buf := bytes.NewBuffer(text) 5276 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5277 c.Assert(err, check.IsNil) 5278 rec := httptest.NewRecorder() 5279 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5280 c.Check(rec.Code, check.Equals, 400) 5281 var body map[string]interface{} 5282 err = json.Unmarshal(rec.Body.Bytes(), &body) 5283 c.Check(err, check.IsNil) 5284 c.Check(body, check.DeepEquals, map[string]interface{}{ 5285 "result": map[string]interface{}{ 5286 "message": "snap \"consumer\" has no plug named \"plug\"", 5287 }, 5288 "status": "Bad Request", 5289 "status-code": 400.0, 5290 "type": "error", 5291 }) 5292 } 5293 5294 func (s *apiSuite) TestDisconnectPlugNothingToDo(c *check.C) { 5295 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5296 defer revert() 5297 s.daemon(c) 5298 5299 s.mockSnap(c, consumerYaml) 5300 s.mockSnap(c, producerYaml) 5301 5302 action := &interfaceAction{ 5303 Action: "disconnect", 5304 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5305 Slots: []slotJSON{{Snap: "", Name: ""}}, 5306 } 5307 text, err := json.Marshal(action) 5308 c.Assert(err, check.IsNil) 5309 buf := bytes.NewBuffer(text) 5310 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5311 c.Assert(err, check.IsNil) 5312 rec := httptest.NewRecorder() 5313 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5314 c.Check(rec.Code, check.Equals, 400) 5315 var body map[string]interface{} 5316 err = json.Unmarshal(rec.Body.Bytes(), &body) 5317 c.Check(err, check.IsNil) 5318 c.Check(body, check.DeepEquals, map[string]interface{}{ 5319 "result": map[string]interface{}{ 5320 "message": "nothing to do", 5321 "kind": "interfaces-unchanged", 5322 }, 5323 "status": "Bad Request", 5324 "status-code": 400.0, 5325 "type": "error", 5326 }) 5327 } 5328 5329 func (s *apiSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { 5330 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5331 defer revert() 5332 s.daemon(c) 5333 5334 s.mockSnap(c, consumerYaml) 5335 // there is no producer, no slot defined 5336 5337 action := &interfaceAction{ 5338 Action: "disconnect", 5339 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5340 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5341 } 5342 text, err := json.Marshal(action) 5343 c.Assert(err, check.IsNil) 5344 buf := bytes.NewBuffer(text) 5345 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5346 c.Assert(err, check.IsNil) 5347 rec := httptest.NewRecorder() 5348 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5349 5350 c.Check(rec.Code, check.Equals, 400) 5351 var body map[string]interface{} 5352 err = json.Unmarshal(rec.Body.Bytes(), &body) 5353 c.Check(err, check.IsNil) 5354 c.Check(body, check.DeepEquals, map[string]interface{}{ 5355 "result": map[string]interface{}{ 5356 "message": "snap \"producer\" has no slot named \"slot\"", 5357 }, 5358 "status": "Bad Request", 5359 "status-code": 400.0, 5360 "type": "error", 5361 }) 5362 } 5363 5364 func (s *apiSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { 5365 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5366 defer revert() 5367 s.daemon(c) 5368 5369 s.mockSnap(c, consumerYaml) 5370 s.mockSnap(c, producerYaml) 5371 5372 action := &interfaceAction{ 5373 Action: "disconnect", 5374 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5375 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5376 } 5377 text, err := json.Marshal(action) 5378 c.Assert(err, check.IsNil) 5379 buf := bytes.NewBuffer(text) 5380 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5381 c.Assert(err, check.IsNil) 5382 rec := httptest.NewRecorder() 5383 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5384 5385 c.Check(rec.Code, check.Equals, 400) 5386 var body map[string]interface{} 5387 err = json.Unmarshal(rec.Body.Bytes(), &body) 5388 c.Check(err, check.IsNil) 5389 c.Check(body, check.DeepEquals, map[string]interface{}{ 5390 "result": map[string]interface{}{ 5391 "message": "cannot disconnect consumer:plug from producer:slot, it is not connected", 5392 }, 5393 "status": "Bad Request", 5394 "status-code": 400.0, 5395 "type": "error", 5396 }) 5397 } 5398 5399 func (s *apiSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) { 5400 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5401 defer revert() 5402 s.daemon(c) 5403 5404 s.mockSnap(c, consumerYaml) 5405 s.mockSnap(c, producerYaml) 5406 5407 action := &interfaceAction{ 5408 Action: "disconnect", 5409 Forget: true, 5410 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5411 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5412 } 5413 text, err := json.Marshal(action) 5414 c.Assert(err, check.IsNil) 5415 buf := bytes.NewBuffer(text) 5416 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5417 c.Assert(err, check.IsNil) 5418 rec := httptest.NewRecorder() 5419 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5420 5421 c.Check(rec.Code, check.Equals, 400) 5422 var body map[string]interface{} 5423 err = json.Unmarshal(rec.Body.Bytes(), &body) 5424 c.Check(err, check.IsNil) 5425 c.Check(body, check.DeepEquals, map[string]interface{}{ 5426 "result": map[string]interface{}{ 5427 "message": "cannot forget connection consumer:plug from producer:slot, it was not connected", 5428 }, 5429 "status": "Bad Request", 5430 "status-code": 400.0, 5431 "type": "error", 5432 }) 5433 } 5434 5435 func (s *apiSuite) TestDisconnectConflict(c *check.C) { 5436 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5437 defer revert() 5438 d := s.daemon(c) 5439 5440 s.mockSnap(c, consumerYaml) 5441 s.mockSnap(c, producerYaml) 5442 5443 repo := d.overlord.InterfaceManager().Repository() 5444 connRef := &interfaces.ConnRef{ 5445 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 5446 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 5447 } 5448 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 5449 c.Assert(err, check.IsNil) 5450 5451 st := d.overlord.State() 5452 st.Lock() 5453 st.Set("conns", map[string]interface{}{ 5454 "consumer:plug producer:slot": map[string]interface{}{ 5455 "interface": "test", 5456 }, 5457 }) 5458 st.Unlock() 5459 5460 simulateConflict(d.overlord, "consumer") 5461 5462 action := &interfaceAction{ 5463 Action: "disconnect", 5464 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5465 Slots: []slotJSON{{Snap: "producer", Name: "slot"}}, 5466 } 5467 text, err := json.Marshal(action) 5468 c.Assert(err, check.IsNil) 5469 buf := bytes.NewBuffer(text) 5470 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5471 c.Assert(err, check.IsNil) 5472 rec := httptest.NewRecorder() 5473 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5474 5475 c.Check(rec.Code, check.Equals, 409) 5476 5477 var body map[string]interface{} 5478 err = json.Unmarshal(rec.Body.Bytes(), &body) 5479 c.Check(err, check.IsNil) 5480 c.Check(body, check.DeepEquals, map[string]interface{}{ 5481 "status-code": 409., 5482 "status": "Conflict", 5483 "result": map[string]interface{}{ 5484 "message": `snap "consumer" has "manip" change in progress`, 5485 "kind": "snap-change-conflict", 5486 "value": map[string]interface{}{ 5487 "change-kind": "manip", 5488 "snap-name": "consumer", 5489 }, 5490 }, 5491 "type": "error"}) 5492 } 5493 5494 func (s *apiSuite) TestDisconnectCoreSystemAlias(c *check.C) { 5495 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 5496 defer revert() 5497 d := s.daemon(c) 5498 5499 s.mockSnap(c, consumerYaml) 5500 s.mockSnap(c, coreProducerYaml) 5501 5502 repo := d.overlord.InterfaceManager().Repository() 5503 connRef := &interfaces.ConnRef{ 5504 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 5505 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}, 5506 } 5507 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 5508 c.Assert(err, check.IsNil) 5509 5510 st := d.overlord.State() 5511 st.Lock() 5512 st.Set("conns", map[string]interface{}{ 5513 "consumer:plug core:slot": map[string]interface{}{ 5514 "interface": "test", 5515 }, 5516 }) 5517 st.Unlock() 5518 5519 d.overlord.Loop() 5520 defer d.overlord.Stop() 5521 5522 action := &interfaceAction{ 5523 Action: "disconnect", 5524 Plugs: []plugJSON{{Snap: "consumer", Name: "plug"}}, 5525 Slots: []slotJSON{{Snap: "system", Name: "slot"}}, 5526 } 5527 text, err := json.Marshal(action) 5528 c.Assert(err, check.IsNil) 5529 buf := bytes.NewBuffer(text) 5530 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5531 c.Assert(err, check.IsNil) 5532 rec := httptest.NewRecorder() 5533 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5534 c.Check(rec.Code, check.Equals, 202) 5535 var body map[string]interface{} 5536 err = json.Unmarshal(rec.Body.Bytes(), &body) 5537 c.Check(err, check.IsNil) 5538 id := body["change"].(string) 5539 5540 st.Lock() 5541 chg := st.Change(id) 5542 st.Unlock() 5543 c.Assert(chg, check.NotNil) 5544 5545 <-chg.Ready() 5546 5547 st.Lock() 5548 err = chg.Err() 5549 st.Unlock() 5550 c.Assert(err, check.IsNil) 5551 5552 ifaces := repo.Interfaces() 5553 c.Assert(ifaces.Connections, check.HasLen, 0) 5554 } 5555 5556 func (s *apiSuite) TestUnsupportedInterfaceRequest(c *check.C) { 5557 buf := bytes.NewBuffer([]byte(`garbage`)) 5558 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5559 c.Assert(err, check.IsNil) 5560 rec := httptest.NewRecorder() 5561 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5562 c.Check(rec.Code, check.Equals, 400) 5563 var body map[string]interface{} 5564 err = json.Unmarshal(rec.Body.Bytes(), &body) 5565 c.Check(err, check.IsNil) 5566 c.Check(body, check.DeepEquals, map[string]interface{}{ 5567 "result": map[string]interface{}{ 5568 "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value", 5569 }, 5570 "status": "Bad Request", 5571 "status-code": 400.0, 5572 "type": "error", 5573 }) 5574 } 5575 5576 func (s *apiSuite) TestMissingInterfaceAction(c *check.C) { 5577 action := &interfaceAction{} 5578 text, err := json.Marshal(action) 5579 c.Assert(err, check.IsNil) 5580 buf := bytes.NewBuffer(text) 5581 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5582 c.Assert(err, check.IsNil) 5583 rec := httptest.NewRecorder() 5584 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5585 c.Check(rec.Code, check.Equals, 400) 5586 var body map[string]interface{} 5587 err = json.Unmarshal(rec.Body.Bytes(), &body) 5588 c.Check(err, check.IsNil) 5589 c.Check(body, check.DeepEquals, map[string]interface{}{ 5590 "result": map[string]interface{}{ 5591 "message": "interface action not specified", 5592 }, 5593 "status": "Bad Request", 5594 "status-code": 400.0, 5595 "type": "error", 5596 }) 5597 } 5598 5599 func (s *apiSuite) TestUnsupportedInterfaceAction(c *check.C) { 5600 s.daemon(c) 5601 action := &interfaceAction{Action: "foo"} 5602 text, err := json.Marshal(action) 5603 c.Assert(err, check.IsNil) 5604 buf := bytes.NewBuffer(text) 5605 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 5606 c.Assert(err, check.IsNil) 5607 rec := httptest.NewRecorder() 5608 interfacesCmd.POST(interfacesCmd, req, nil).ServeHTTP(rec, req) 5609 c.Check(rec.Code, check.Equals, 400) 5610 var body map[string]interface{} 5611 err = json.Unmarshal(rec.Body.Bytes(), &body) 5612 c.Check(err, check.IsNil) 5613 c.Check(body, check.DeepEquals, map[string]interface{}{ 5614 "result": map[string]interface{}{ 5615 "message": "unsupported interface action: \"foo\"", 5616 }, 5617 "status": "Bad Request", 5618 "status-code": 400.0, 5619 "type": "error", 5620 }) 5621 } 5622 5623 func setupChanges(st *state.State) []string { 5624 chg1 := st.NewChange("install", "install...") 5625 chg1.Set("snap-names", []string{"funky-snap-name"}) 5626 t1 := st.NewTask("download", "1...") 5627 t2 := st.NewTask("activate", "2...") 5628 chg1.AddAll(state.NewTaskSet(t1, t2)) 5629 t1.Logf("l11") 5630 t1.Logf("l12") 5631 chg2 := st.NewChange("remove", "remove..") 5632 t3 := st.NewTask("unlink", "1...") 5633 chg2.AddTask(t3) 5634 t3.SetStatus(state.ErrorStatus) 5635 t3.Errorf("rm failed") 5636 5637 return []string{chg1.ID(), chg2.ID(), t1.ID(), t2.ID(), t3.ID()} 5638 } 5639 5640 func (s *apiSuite) TestStateChangesDefaultToInProgress(c *check.C) { 5641 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5642 defer restore() 5643 5644 // Setup 5645 d := newTestDaemon(c) 5646 st := d.overlord.State() 5647 st.Lock() 5648 setupChanges(st) 5649 st.Unlock() 5650 5651 // Execute 5652 req, err := http.NewRequest("GET", "/v2/changes", nil) 5653 c.Assert(err, check.IsNil) 5654 rsp := getChanges(stateChangesCmd, req, nil).(*resp) 5655 5656 // Verify 5657 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 5658 c.Check(rsp.Status, check.Equals, 200) 5659 c.Assert(rsp.Result, check.HasLen, 1) 5660 5661 res, err := rsp.MarshalJSON() 5662 c.Assert(err, check.IsNil) 5663 5664 c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*`) 5665 } 5666 5667 func (s *apiSuite) TestStateChangesInProgress(c *check.C) { 5668 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5669 defer restore() 5670 5671 // Setup 5672 d := newTestDaemon(c) 5673 st := d.overlord.State() 5674 st.Lock() 5675 setupChanges(st) 5676 st.Unlock() 5677 5678 // Execute 5679 req, err := http.NewRequest("GET", "/v2/changes?select=in-progress", nil) 5680 c.Assert(err, check.IsNil) 5681 rsp := getChanges(stateChangesCmd, req, nil).(*resp) 5682 5683 // Verify 5684 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 5685 c.Check(rsp.Status, check.Equals, 200) 5686 c.Assert(rsp.Result, check.HasLen, 1) 5687 5688 res, err := rsp.MarshalJSON() 5689 c.Assert(err, check.IsNil) 5690 5691 c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`) 5692 } 5693 5694 func (s *apiSuite) TestStateChangesAll(c *check.C) { 5695 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5696 defer restore() 5697 5698 // Setup 5699 d := newTestDaemon(c) 5700 st := d.overlord.State() 5701 st.Lock() 5702 setupChanges(st) 5703 st.Unlock() 5704 5705 // Execute 5706 req, err := http.NewRequest("GET", "/v2/changes?select=all", nil) 5707 c.Assert(err, check.IsNil) 5708 rsp := getChanges(stateChangesCmd, req, nil).(*resp) 5709 5710 // Verify 5711 c.Check(rsp.Status, check.Equals, 200) 5712 c.Assert(rsp.Result, check.HasLen, 2) 5713 5714 res, err := rsp.MarshalJSON() 5715 c.Assert(err, check.IsNil) 5716 5717 c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"install","summary":"install...","status":"Do","tasks":\[{"id":"\w+","kind":"download","summary":"1...","status":"Do","log":\["2016-04-21T01:02:03Z INFO l11","2016-04-21T01:02:03Z INFO l12"],"progress":{"label":"","done":0,"total":1},"spawn-time":"2016-04-21T01:02:03Z"}.*],"ready":false,"spawn-time":"2016-04-21T01:02:03Z"}.*`) 5718 c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`) 5719 } 5720 5721 func (s *apiSuite) TestStateChangesReady(c *check.C) { 5722 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5723 defer restore() 5724 5725 // Setup 5726 d := newTestDaemon(c) 5727 st := d.overlord.State() 5728 st.Lock() 5729 setupChanges(st) 5730 st.Unlock() 5731 5732 // Execute 5733 req, err := http.NewRequest("GET", "/v2/changes?select=ready", nil) 5734 c.Assert(err, check.IsNil) 5735 rsp := getChanges(stateChangesCmd, req, nil).(*resp) 5736 5737 // Verify 5738 c.Check(rsp.Status, check.Equals, 200) 5739 c.Assert(rsp.Result, check.HasLen, 1) 5740 5741 res, err := rsp.MarshalJSON() 5742 c.Assert(err, check.IsNil) 5743 5744 c.Check(string(res), check.Matches, `.*{"id":"\w+","kind":"remove","summary":"remove..","status":"Error","tasks":\[{"id":"\w+","kind":"unlink","summary":"1...","status":"Error","log":\["2016-04-21T01:02:03Z ERROR rm failed"],"progress":{"label":"","done":1,"total":1},"spawn-time":"2016-04-21T01:02:03Z","ready-time":"2016-04-21T01:02:03Z"}.*],"ready":true,"err":"[^"]+".*`) 5745 } 5746 5747 func (s *apiSuite) TestStateChangesForSnapName(c *check.C) { 5748 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5749 defer restore() 5750 5751 // Setup 5752 d := newTestDaemon(c) 5753 st := d.overlord.State() 5754 st.Lock() 5755 setupChanges(st) 5756 st.Unlock() 5757 5758 // Execute 5759 req, err := http.NewRequest("GET", "/v2/changes?for=funky-snap-name&select=all", nil) 5760 c.Assert(err, check.IsNil) 5761 rsp := getChanges(stateChangesCmd, req, nil).(*resp) 5762 5763 // Verify 5764 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 5765 c.Check(rsp.Status, check.Equals, 200) 5766 c.Assert(rsp.Result, check.FitsTypeOf, []*changeInfo(nil)) 5767 5768 res := rsp.Result.([]*changeInfo) 5769 c.Assert(res, check.HasLen, 1) 5770 c.Check(res[0].Kind, check.Equals, `install`) 5771 5772 _, err = rsp.MarshalJSON() 5773 c.Assert(err, check.IsNil) 5774 } 5775 5776 func (s *apiSuite) TestStateChangesForSnapNameWithApp(c *check.C) { 5777 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5778 defer restore() 5779 5780 // Setup 5781 d := newTestDaemon(c) 5782 st := d.overlord.State() 5783 st.Lock() 5784 chg1 := st.NewChange("service-control", "install...") 5785 // as triggered by snap restart lxd.daemon 5786 chg1.Set("snap-names", []string{"lxd.daemon"}) 5787 t1 := st.NewTask("exec-command", "1...") 5788 chg1.AddAll(state.NewTaskSet(t1)) 5789 t1.Logf("foobar") 5790 5791 st.Unlock() 5792 5793 // Execute 5794 req, err := http.NewRequest("GET", "/v2/changes?for=lxd&select=all", nil) 5795 c.Assert(err, check.IsNil) 5796 rsp := getChanges(stateChangesCmd, req, nil).(*resp) 5797 5798 // Verify 5799 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 5800 c.Check(rsp.Status, check.Equals, 200) 5801 c.Assert(rsp.Result, check.FitsTypeOf, []*changeInfo(nil)) 5802 5803 res := rsp.Result.([]*changeInfo) 5804 c.Assert(res, check.HasLen, 1) 5805 c.Check(res[0].Kind, check.Equals, `service-control`) 5806 5807 _, err = rsp.MarshalJSON() 5808 c.Assert(err, check.IsNil) 5809 } 5810 5811 func (s *apiSuite) TestStateChange(c *check.C) { 5812 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5813 defer restore() 5814 5815 // Setup 5816 d := newTestDaemon(c) 5817 st := d.overlord.State() 5818 st.Lock() 5819 ids := setupChanges(st) 5820 chg := st.Change(ids[0]) 5821 chg.Set("api-data", map[string]int{"n": 42}) 5822 st.Unlock() 5823 s.vars = map[string]string{"id": ids[0]} 5824 5825 // Execute 5826 req, err := http.NewRequest("POST", "/v2/change/"+ids[0], nil) 5827 c.Assert(err, check.IsNil) 5828 rsp := getChange(stateChangeCmd, req, nil).(*resp) 5829 rec := httptest.NewRecorder() 5830 rsp.ServeHTTP(rec, req) 5831 5832 // Verify 5833 c.Check(rec.Code, check.Equals, 200) 5834 c.Check(rsp.Status, check.Equals, 200) 5835 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 5836 c.Check(rsp.Result, check.NotNil) 5837 5838 var body map[string]interface{} 5839 err = json.Unmarshal(rec.Body.Bytes(), &body) 5840 c.Check(err, check.IsNil) 5841 c.Check(body["result"], check.DeepEquals, map[string]interface{}{ 5842 "id": ids[0], 5843 "kind": "install", 5844 "summary": "install...", 5845 "status": "Do", 5846 "ready": false, 5847 "spawn-time": "2016-04-21T01:02:03Z", 5848 "tasks": []interface{}{ 5849 map[string]interface{}{ 5850 "id": ids[2], 5851 "kind": "download", 5852 "summary": "1...", 5853 "status": "Do", 5854 "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"}, 5855 "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.}, 5856 "spawn-time": "2016-04-21T01:02:03Z", 5857 }, 5858 map[string]interface{}{ 5859 "id": ids[3], 5860 "kind": "activate", 5861 "summary": "2...", 5862 "status": "Do", 5863 "progress": map[string]interface{}{"label": "", "done": 0., "total": 1.}, 5864 "spawn-time": "2016-04-21T01:02:03Z", 5865 }, 5866 }, 5867 "data": map[string]interface{}{ 5868 "n": float64(42), 5869 }, 5870 }) 5871 } 5872 5873 func (s *apiSuite) TestStateChangeAbort(c *check.C) { 5874 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5875 defer restore() 5876 5877 soon := 0 5878 ensureStateSoon = func(st *state.State) { 5879 soon++ 5880 } 5881 5882 // Setup 5883 d := newTestDaemon(c) 5884 st := d.overlord.State() 5885 st.Lock() 5886 ids := setupChanges(st) 5887 st.Unlock() 5888 s.vars = map[string]string{"id": ids[0]} 5889 5890 buf := bytes.NewBufferString(`{"action": "abort"}`) 5891 5892 // Execute 5893 req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf) 5894 c.Assert(err, check.IsNil) 5895 rsp := abortChange(stateChangeCmd, req, nil).(*resp) 5896 rec := httptest.NewRecorder() 5897 rsp.ServeHTTP(rec, req) 5898 5899 // Ensure scheduled 5900 c.Check(soon, check.Equals, 1) 5901 5902 // Verify 5903 c.Check(rec.Code, check.Equals, 200) 5904 c.Check(rsp.Status, check.Equals, 200) 5905 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 5906 c.Check(rsp.Result, check.NotNil) 5907 5908 var body map[string]interface{} 5909 err = json.Unmarshal(rec.Body.Bytes(), &body) 5910 c.Check(err, check.IsNil) 5911 c.Check(body["result"], check.DeepEquals, map[string]interface{}{ 5912 "id": ids[0], 5913 "kind": "install", 5914 "summary": "install...", 5915 "status": "Hold", 5916 "ready": true, 5917 "spawn-time": "2016-04-21T01:02:03Z", 5918 "ready-time": "2016-04-21T01:02:03Z", 5919 "tasks": []interface{}{ 5920 map[string]interface{}{ 5921 "id": ids[2], 5922 "kind": "download", 5923 "summary": "1...", 5924 "status": "Hold", 5925 "log": []interface{}{"2016-04-21T01:02:03Z INFO l11", "2016-04-21T01:02:03Z INFO l12"}, 5926 "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.}, 5927 "spawn-time": "2016-04-21T01:02:03Z", 5928 "ready-time": "2016-04-21T01:02:03Z", 5929 }, 5930 map[string]interface{}{ 5931 "id": ids[3], 5932 "kind": "activate", 5933 "summary": "2...", 5934 "status": "Hold", 5935 "progress": map[string]interface{}{"label": "", "done": 1., "total": 1.}, 5936 "spawn-time": "2016-04-21T01:02:03Z", 5937 "ready-time": "2016-04-21T01:02:03Z", 5938 }, 5939 }, 5940 }) 5941 } 5942 5943 func (s *apiSuite) TestStateChangeAbortIsReady(c *check.C) { 5944 restore := state.MockTime(time.Date(2016, 04, 21, 1, 2, 3, 0, time.UTC)) 5945 defer restore() 5946 5947 // Setup 5948 d := newTestDaemon(c) 5949 st := d.overlord.State() 5950 st.Lock() 5951 ids := setupChanges(st) 5952 st.Change(ids[0]).SetStatus(state.DoneStatus) 5953 st.Unlock() 5954 s.vars = map[string]string{"id": ids[0]} 5955 5956 buf := bytes.NewBufferString(`{"action": "abort"}`) 5957 5958 // Execute 5959 req, err := http.NewRequest("POST", "/v2/changes/"+ids[0], buf) 5960 c.Assert(err, check.IsNil) 5961 rsp := abortChange(stateChangeCmd, req, nil).(*resp) 5962 rec := httptest.NewRecorder() 5963 rsp.ServeHTTP(rec, req) 5964 5965 // Verify 5966 c.Check(rec.Code, check.Equals, 400) 5967 c.Check(rsp.Status, check.Equals, 400) 5968 c.Check(rsp.Type, check.Equals, ResponseTypeError) 5969 c.Check(rsp.Result, check.NotNil) 5970 5971 var body map[string]interface{} 5972 err = json.Unmarshal(rec.Body.Bytes(), &body) 5973 c.Check(err, check.IsNil) 5974 c.Check(body["result"], check.DeepEquals, map[string]interface{}{ 5975 "message": fmt.Sprintf("cannot abort change %s with nothing pending", ids[0]), 5976 }) 5977 } 5978 5979 const validBuyInput = `{ 5980 "snap-id": "the-snap-id-1234abcd", 5981 "snap-name": "the snap name", 5982 "price": 1.23, 5983 "currency": "EUR" 5984 }` 5985 5986 var validBuyOptions = &client.BuyOptions{ 5987 SnapID: "the-snap-id-1234abcd", 5988 Price: 1.23, 5989 Currency: "EUR", 5990 } 5991 5992 var buyTests = []struct { 5993 input string 5994 result *client.BuyResult 5995 err error 5996 expectedStatus int 5997 expectedResult interface{} 5998 expectedResponseType ResponseType 5999 expectedBuyOptions *client.BuyOptions 6000 }{ 6001 { 6002 // Success 6003 input: validBuyInput, 6004 result: &client.BuyResult{ 6005 State: "Complete", 6006 }, 6007 expectedStatus: 200, 6008 expectedResult: &client.BuyResult{ 6009 State: "Complete", 6010 }, 6011 expectedResponseType: ResponseTypeSync, 6012 expectedBuyOptions: validBuyOptions, 6013 }, 6014 { 6015 // Fail with internal error 6016 input: `{ 6017 "snap-id": "the-snap-id-1234abcd", 6018 "price": 1.23, 6019 "currency": "EUR" 6020 }`, 6021 err: fmt.Errorf("internal error banana"), 6022 expectedStatus: 500, 6023 expectedResponseType: ResponseTypeError, 6024 expectedResult: &errorResult{ 6025 Message: "internal error banana", 6026 }, 6027 expectedBuyOptions: &client.BuyOptions{ 6028 SnapID: "the-snap-id-1234abcd", 6029 Price: 1.23, 6030 Currency: "EUR", 6031 }, 6032 }, 6033 { 6034 // Fail with unauthenticated error 6035 input: validBuyInput, 6036 err: store.ErrUnauthenticated, 6037 expectedStatus: 400, 6038 expectedResponseType: ResponseTypeError, 6039 expectedResult: &errorResult{ 6040 Message: "you need to log in first", 6041 Kind: "login-required", 6042 }, 6043 expectedBuyOptions: validBuyOptions, 6044 }, 6045 { 6046 // Fail with TOS not accepted 6047 input: validBuyInput, 6048 err: store.ErrTOSNotAccepted, 6049 expectedStatus: 400, 6050 expectedResponseType: ResponseTypeError, 6051 expectedResult: &errorResult{ 6052 Message: "terms of service not accepted", 6053 Kind: "terms-not-accepted", 6054 }, 6055 expectedBuyOptions: validBuyOptions, 6056 }, 6057 { 6058 // Fail with no payment methods 6059 input: validBuyInput, 6060 err: store.ErrNoPaymentMethods, 6061 expectedStatus: 400, 6062 expectedResponseType: ResponseTypeError, 6063 expectedResult: &errorResult{ 6064 Message: "no payment methods", 6065 Kind: "no-payment-methods", 6066 }, 6067 expectedBuyOptions: validBuyOptions, 6068 }, 6069 { 6070 // Fail with payment declined 6071 input: validBuyInput, 6072 err: store.ErrPaymentDeclined, 6073 expectedStatus: 400, 6074 expectedResponseType: ResponseTypeError, 6075 expectedResult: &errorResult{ 6076 Message: "payment declined", 6077 Kind: "payment-declined", 6078 }, 6079 expectedBuyOptions: validBuyOptions, 6080 }, 6081 } 6082 6083 func (s *apiSuite) TestBuySnap(c *check.C) { 6084 s.daemon(c) 6085 6086 for _, test := range buyTests { 6087 s.buyResult = test.result 6088 s.err = test.err 6089 6090 buf := bytes.NewBufferString(test.input) 6091 req, err := http.NewRequest("POST", "/v2/buy", buf) 6092 c.Assert(err, check.IsNil) 6093 6094 state := snapCmd.d.overlord.State() 6095 state.Lock() 6096 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 6097 state.Unlock() 6098 c.Check(err, check.IsNil) 6099 6100 rsp := postBuy(buyCmd, req, user).(*resp) 6101 6102 c.Check(rsp.Status, check.Equals, test.expectedStatus) 6103 c.Check(rsp.Type, check.Equals, test.expectedResponseType) 6104 c.Assert(rsp.Result, check.FitsTypeOf, test.expectedResult) 6105 c.Check(rsp.Result, check.DeepEquals, test.expectedResult) 6106 6107 c.Check(s.buyOptions, check.DeepEquals, test.expectedBuyOptions) 6108 c.Check(s.user, check.Equals, user) 6109 } 6110 } 6111 6112 func (s *apiSuite) TestIsTrue(c *check.C) { 6113 form := &multipart.Form{} 6114 c.Check(isTrue(form, "foo"), check.Equals, false) 6115 for _, f := range []string{"", "false", "0", "False", "f", "try"} { 6116 form.Value = map[string][]string{"foo": {f}} 6117 c.Check(isTrue(form, "foo"), check.Equals, false, check.Commentf("expected %q to be false", f)) 6118 } 6119 for _, t := range []string{"true", "1", "True", "t"} { 6120 form.Value = map[string][]string{"foo": {t}} 6121 c.Check(isTrue(form, "foo"), check.Equals, true, check.Commentf("expected %q to be true", t)) 6122 } 6123 } 6124 6125 var readyToBuyTests = []struct { 6126 input error 6127 status int 6128 respType interface{} 6129 response interface{} 6130 }{ 6131 { 6132 // Success 6133 input: nil, 6134 status: 200, 6135 respType: ResponseTypeSync, 6136 response: true, 6137 }, 6138 { 6139 // Not accepted TOS 6140 input: store.ErrTOSNotAccepted, 6141 status: 400, 6142 respType: ResponseTypeError, 6143 response: &errorResult{ 6144 Message: "terms of service not accepted", 6145 Kind: client.ErrorKindTermsNotAccepted, 6146 }, 6147 }, 6148 { 6149 // No payment methods 6150 input: store.ErrNoPaymentMethods, 6151 status: 400, 6152 respType: ResponseTypeError, 6153 response: &errorResult{ 6154 Message: "no payment methods", 6155 Kind: client.ErrorKindNoPaymentMethods, 6156 }, 6157 }, 6158 } 6159 6160 func (s *apiSuite) TestReadyToBuy(c *check.C) { 6161 s.daemon(c) 6162 6163 for _, test := range readyToBuyTests { 6164 s.err = test.input 6165 6166 req, err := http.NewRequest("GET", "/v2/buy/ready", nil) 6167 c.Assert(err, check.IsNil) 6168 6169 state := snapCmd.d.overlord.State() 6170 state.Lock() 6171 user, err := auth.NewUser(state, "username", "email@test.com", "macaroon", []string{"discharge"}) 6172 state.Unlock() 6173 c.Check(err, check.IsNil) 6174 6175 rsp := readyToBuy(readyToBuyCmd, req, user).(*resp) 6176 c.Check(rsp.Status, check.Equals, test.status) 6177 c.Check(rsp.Type, check.Equals, test.respType) 6178 c.Assert(rsp.Result, check.FitsTypeOf, test.response) 6179 c.Check(rsp.Result, check.DeepEquals, test.response) 6180 } 6181 } 6182 6183 // aliases 6184 6185 func (s *apiSuite) TestAliasSuccess(c *check.C) { 6186 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6187 c.Assert(err, check.IsNil) 6188 d := s.daemon(c) 6189 6190 s.mockSnap(c, aliasYaml) 6191 6192 oldAutoAliases := snapstate.AutoAliases 6193 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6194 return nil, nil 6195 } 6196 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6197 6198 d.overlord.Loop() 6199 defer d.overlord.Stop() 6200 6201 action := &aliasAction{ 6202 Action: "alias", 6203 Snap: "alias-snap", 6204 App: "app", 6205 Alias: "alias1", 6206 } 6207 text, err := json.Marshal(action) 6208 c.Assert(err, check.IsNil) 6209 buf := bytes.NewBuffer(text) 6210 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6211 c.Assert(err, check.IsNil) 6212 rec := httptest.NewRecorder() 6213 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6214 c.Assert(rec.Code, check.Equals, 202) 6215 var body map[string]interface{} 6216 err = json.Unmarshal(rec.Body.Bytes(), &body) 6217 c.Check(err, check.IsNil) 6218 id := body["change"].(string) 6219 6220 st := d.overlord.State() 6221 st.Lock() 6222 chg := st.Change(id) 6223 st.Unlock() 6224 c.Assert(chg, check.NotNil) 6225 6226 <-chg.Ready() 6227 6228 st.Lock() 6229 err = chg.Err() 6230 st.Unlock() 6231 c.Assert(err, check.IsNil) 6232 6233 // sanity check 6234 c.Check(osutil.IsSymlink(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, true) 6235 } 6236 6237 func (s *apiSuite) TestAliasChangeConflict(c *check.C) { 6238 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6239 c.Assert(err, check.IsNil) 6240 d := s.daemon(c) 6241 6242 s.mockSnap(c, aliasYaml) 6243 6244 simulateConflict(d.overlord, "alias-snap") 6245 6246 oldAutoAliases := snapstate.AutoAliases 6247 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6248 return nil, nil 6249 } 6250 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6251 6252 action := &aliasAction{ 6253 Action: "alias", 6254 Snap: "alias-snap", 6255 App: "app", 6256 Alias: "alias1", 6257 } 6258 text, err := json.Marshal(action) 6259 c.Assert(err, check.IsNil) 6260 buf := bytes.NewBuffer(text) 6261 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6262 c.Assert(err, check.IsNil) 6263 rec := httptest.NewRecorder() 6264 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6265 c.Check(rec.Code, check.Equals, 409) 6266 6267 var body map[string]interface{} 6268 err = json.Unmarshal(rec.Body.Bytes(), &body) 6269 c.Check(err, check.IsNil) 6270 c.Check(body, check.DeepEquals, map[string]interface{}{ 6271 "status-code": 409., 6272 "status": "Conflict", 6273 "result": map[string]interface{}{ 6274 "message": `snap "alias-snap" has "manip" change in progress`, 6275 "kind": "snap-change-conflict", 6276 "value": map[string]interface{}{ 6277 "change-kind": "manip", 6278 "snap-name": "alias-snap", 6279 }, 6280 }, 6281 "type": "error"}) 6282 } 6283 6284 func (s *apiSuite) TestAliasErrors(c *check.C) { 6285 s.daemon(c) 6286 6287 errScenarios := []struct { 6288 mangle func(*aliasAction) 6289 err string 6290 }{ 6291 {func(a *aliasAction) { a.Action = "" }, `unsupported alias action: ""`}, 6292 {func(a *aliasAction) { a.Action = "what" }, `unsupported alias action: "what"`}, 6293 {func(a *aliasAction) { a.Snap = "lalala" }, `snap "lalala" is not installed`}, 6294 {func(a *aliasAction) { a.Alias = ".foo" }, `invalid alias name: ".foo"`}, 6295 {func(a *aliasAction) { a.Aliases = []string{"baz"} }, `cannot interpret request, snaps can no longer be expected to declare their aliases`}, 6296 } 6297 6298 for _, scen := range errScenarios { 6299 action := &aliasAction{ 6300 Action: "alias", 6301 Snap: "alias-snap", 6302 App: "app", 6303 Alias: "alias1", 6304 } 6305 scen.mangle(action) 6306 6307 text, err := json.Marshal(action) 6308 c.Assert(err, check.IsNil) 6309 buf := bytes.NewBuffer(text) 6310 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6311 c.Assert(err, check.IsNil) 6312 6313 rsp := changeAliases(aliasesCmd, req, nil).(*resp) 6314 c.Check(rsp.Type, check.Equals, ResponseTypeError) 6315 c.Check(rsp.Status, check.Equals, 400) 6316 c.Check(rsp.Result.(*errorResult).Message, check.Matches, scen.err) 6317 } 6318 } 6319 6320 func (s *apiSuite) TestUnaliasSnapSuccess(c *check.C) { 6321 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6322 c.Assert(err, check.IsNil) 6323 d := s.daemon(c) 6324 6325 s.mockSnap(c, aliasYaml) 6326 6327 oldAutoAliases := snapstate.AutoAliases 6328 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6329 return nil, nil 6330 } 6331 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6332 6333 d.overlord.Loop() 6334 defer d.overlord.Stop() 6335 6336 action := &aliasAction{ 6337 Action: "unalias", 6338 Snap: "alias-snap", 6339 } 6340 text, err := json.Marshal(action) 6341 c.Assert(err, check.IsNil) 6342 buf := bytes.NewBuffer(text) 6343 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6344 c.Assert(err, check.IsNil) 6345 rec := httptest.NewRecorder() 6346 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6347 c.Assert(rec.Code, check.Equals, 202) 6348 var body map[string]interface{} 6349 err = json.Unmarshal(rec.Body.Bytes(), &body) 6350 c.Check(err, check.IsNil) 6351 id := body["change"].(string) 6352 6353 st := d.overlord.State() 6354 st.Lock() 6355 chg := st.Change(id) 6356 c.Check(chg.Summary(), check.Equals, `Disable all aliases for snap "alias-snap"`) 6357 st.Unlock() 6358 c.Assert(chg, check.NotNil) 6359 6360 <-chg.Ready() 6361 6362 st.Lock() 6363 defer st.Unlock() 6364 err = chg.Err() 6365 c.Assert(err, check.IsNil) 6366 6367 // sanity check 6368 var snapst snapstate.SnapState 6369 err = snapstate.Get(st, "alias-snap", &snapst) 6370 c.Assert(err, check.IsNil) 6371 c.Check(snapst.AutoAliasesDisabled, check.Equals, true) 6372 } 6373 6374 func (s *apiSuite) TestUnaliasDWIMSnapSuccess(c *check.C) { 6375 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6376 c.Assert(err, check.IsNil) 6377 d := s.daemon(c) 6378 6379 s.mockSnap(c, aliasYaml) 6380 6381 oldAutoAliases := snapstate.AutoAliases 6382 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6383 return nil, nil 6384 } 6385 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6386 6387 d.overlord.Loop() 6388 defer d.overlord.Stop() 6389 6390 action := &aliasAction{ 6391 Action: "unalias", 6392 Snap: "alias-snap", 6393 Alias: "alias-snap", 6394 } 6395 text, err := json.Marshal(action) 6396 c.Assert(err, check.IsNil) 6397 buf := bytes.NewBuffer(text) 6398 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6399 c.Assert(err, check.IsNil) 6400 rec := httptest.NewRecorder() 6401 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6402 c.Assert(rec.Code, check.Equals, 202) 6403 var body map[string]interface{} 6404 err = json.Unmarshal(rec.Body.Bytes(), &body) 6405 c.Check(err, check.IsNil) 6406 id := body["change"].(string) 6407 6408 st := d.overlord.State() 6409 st.Lock() 6410 chg := st.Change(id) 6411 c.Check(chg.Summary(), check.Equals, `Disable all aliases for snap "alias-snap"`) 6412 st.Unlock() 6413 c.Assert(chg, check.NotNil) 6414 6415 <-chg.Ready() 6416 6417 st.Lock() 6418 defer st.Unlock() 6419 err = chg.Err() 6420 c.Assert(err, check.IsNil) 6421 6422 // sanity check 6423 var snapst snapstate.SnapState 6424 err = snapstate.Get(st, "alias-snap", &snapst) 6425 c.Assert(err, check.IsNil) 6426 c.Check(snapst.AutoAliasesDisabled, check.Equals, true) 6427 } 6428 6429 func (s *apiSuite) TestUnaliasAliasSuccess(c *check.C) { 6430 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6431 c.Assert(err, check.IsNil) 6432 d := s.daemon(c) 6433 6434 s.mockSnap(c, aliasYaml) 6435 6436 oldAutoAliases := snapstate.AutoAliases 6437 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6438 return nil, nil 6439 } 6440 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6441 6442 d.overlord.Loop() 6443 defer d.overlord.Stop() 6444 6445 action := &aliasAction{ 6446 Action: "alias", 6447 Snap: "alias-snap", 6448 App: "app", 6449 Alias: "alias1", 6450 } 6451 text, err := json.Marshal(action) 6452 c.Assert(err, check.IsNil) 6453 buf := bytes.NewBuffer(text) 6454 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6455 c.Assert(err, check.IsNil) 6456 rec := httptest.NewRecorder() 6457 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6458 c.Assert(rec.Code, check.Equals, 202) 6459 var body map[string]interface{} 6460 err = json.Unmarshal(rec.Body.Bytes(), &body) 6461 c.Check(err, check.IsNil) 6462 id := body["change"].(string) 6463 6464 st := d.overlord.State() 6465 st.Lock() 6466 chg := st.Change(id) 6467 st.Unlock() 6468 c.Assert(chg, check.NotNil) 6469 6470 <-chg.Ready() 6471 6472 st.Lock() 6473 err = chg.Err() 6474 st.Unlock() 6475 c.Assert(err, check.IsNil) 6476 6477 // unalias 6478 action = &aliasAction{ 6479 Action: "unalias", 6480 Alias: "alias1", 6481 } 6482 text, err = json.Marshal(action) 6483 c.Assert(err, check.IsNil) 6484 buf = bytes.NewBuffer(text) 6485 req, err = http.NewRequest("POST", "/v2/aliases", buf) 6486 c.Assert(err, check.IsNil) 6487 rec = httptest.NewRecorder() 6488 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6489 c.Assert(rec.Code, check.Equals, 202) 6490 err = json.Unmarshal(rec.Body.Bytes(), &body) 6491 c.Check(err, check.IsNil) 6492 id = body["change"].(string) 6493 6494 st.Lock() 6495 chg = st.Change(id) 6496 c.Check(chg.Summary(), check.Equals, `Remove manual alias "alias1" for snap "alias-snap"`) 6497 st.Unlock() 6498 c.Assert(chg, check.NotNil) 6499 6500 <-chg.Ready() 6501 6502 st.Lock() 6503 defer st.Unlock() 6504 err = chg.Err() 6505 c.Assert(err, check.IsNil) 6506 6507 // sanity check 6508 c.Check(osutil.FileExists(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, false) 6509 } 6510 6511 func (s *apiSuite) TestUnaliasDWIMAliasSuccess(c *check.C) { 6512 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6513 c.Assert(err, check.IsNil) 6514 d := s.daemon(c) 6515 6516 s.mockSnap(c, aliasYaml) 6517 6518 oldAutoAliases := snapstate.AutoAliases 6519 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6520 return nil, nil 6521 } 6522 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6523 6524 d.overlord.Loop() 6525 defer d.overlord.Stop() 6526 6527 action := &aliasAction{ 6528 Action: "alias", 6529 Snap: "alias-snap", 6530 App: "app", 6531 Alias: "alias1", 6532 } 6533 text, err := json.Marshal(action) 6534 c.Assert(err, check.IsNil) 6535 buf := bytes.NewBuffer(text) 6536 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6537 c.Assert(err, check.IsNil) 6538 rec := httptest.NewRecorder() 6539 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6540 c.Assert(rec.Code, check.Equals, 202) 6541 var body map[string]interface{} 6542 err = json.Unmarshal(rec.Body.Bytes(), &body) 6543 c.Check(err, check.IsNil) 6544 id := body["change"].(string) 6545 6546 st := d.overlord.State() 6547 st.Lock() 6548 chg := st.Change(id) 6549 st.Unlock() 6550 c.Assert(chg, check.NotNil) 6551 6552 <-chg.Ready() 6553 6554 st.Lock() 6555 err = chg.Err() 6556 st.Unlock() 6557 c.Assert(err, check.IsNil) 6558 6559 // DWIM unalias an alias 6560 action = &aliasAction{ 6561 Action: "unalias", 6562 Snap: "alias1", 6563 Alias: "alias1", 6564 } 6565 text, err = json.Marshal(action) 6566 c.Assert(err, check.IsNil) 6567 buf = bytes.NewBuffer(text) 6568 req, err = http.NewRequest("POST", "/v2/aliases", buf) 6569 c.Assert(err, check.IsNil) 6570 rec = httptest.NewRecorder() 6571 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6572 c.Assert(rec.Code, check.Equals, 202) 6573 err = json.Unmarshal(rec.Body.Bytes(), &body) 6574 c.Check(err, check.IsNil) 6575 id = body["change"].(string) 6576 6577 st.Lock() 6578 chg = st.Change(id) 6579 c.Check(chg.Summary(), check.Equals, `Remove manual alias "alias1" for snap "alias-snap"`) 6580 st.Unlock() 6581 c.Assert(chg, check.NotNil) 6582 6583 <-chg.Ready() 6584 6585 st.Lock() 6586 defer st.Unlock() 6587 err = chg.Err() 6588 c.Assert(err, check.IsNil) 6589 6590 // sanity check 6591 c.Check(osutil.FileExists(filepath.Join(dirs.SnapBinariesDir, "alias1")), check.Equals, false) 6592 } 6593 6594 func (s *apiSuite) TestPreferSuccess(c *check.C) { 6595 err := os.MkdirAll(dirs.SnapBinariesDir, 0755) 6596 c.Assert(err, check.IsNil) 6597 d := s.daemon(c) 6598 6599 s.mockSnap(c, aliasYaml) 6600 6601 oldAutoAliases := snapstate.AutoAliases 6602 snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) { 6603 return nil, nil 6604 } 6605 defer func() { snapstate.AutoAliases = oldAutoAliases }() 6606 6607 d.overlord.Loop() 6608 defer d.overlord.Stop() 6609 6610 action := &aliasAction{ 6611 Action: "prefer", 6612 Snap: "alias-snap", 6613 } 6614 text, err := json.Marshal(action) 6615 c.Assert(err, check.IsNil) 6616 buf := bytes.NewBuffer(text) 6617 req, err := http.NewRequest("POST", "/v2/aliases", buf) 6618 c.Assert(err, check.IsNil) 6619 rec := httptest.NewRecorder() 6620 aliasesCmd.POST(aliasesCmd, req, nil).ServeHTTP(rec, req) 6621 c.Assert(rec.Code, check.Equals, 202) 6622 var body map[string]interface{} 6623 err = json.Unmarshal(rec.Body.Bytes(), &body) 6624 c.Check(err, check.IsNil) 6625 id := body["change"].(string) 6626 6627 st := d.overlord.State() 6628 st.Lock() 6629 chg := st.Change(id) 6630 c.Check(chg.Summary(), check.Equals, `Prefer aliases of snap "alias-snap"`) 6631 st.Unlock() 6632 c.Assert(chg, check.NotNil) 6633 6634 <-chg.Ready() 6635 6636 st.Lock() 6637 defer st.Unlock() 6638 err = chg.Err() 6639 c.Assert(err, check.IsNil) 6640 6641 // sanity check 6642 var snapst snapstate.SnapState 6643 err = snapstate.Get(st, "alias-snap", &snapst) 6644 c.Assert(err, check.IsNil) 6645 c.Check(snapst.AutoAliasesDisabled, check.Equals, false) 6646 } 6647 6648 func (s *apiSuite) TestAliases(c *check.C) { 6649 d := s.daemon(c) 6650 6651 st := d.overlord.State() 6652 st.Lock() 6653 snapstate.Set(st, "alias-snap1", &snapstate.SnapState{ 6654 Sequence: []*snap.SideInfo{ 6655 {RealName: "alias-snap1", Revision: snap.R(11)}, 6656 }, 6657 Current: snap.R(11), 6658 Active: true, 6659 Aliases: map[string]*snapstate.AliasTarget{ 6660 "alias1": {Manual: "cmd1x", Auto: "cmd1"}, 6661 "alias2": {Auto: "cmd2"}, 6662 }, 6663 }) 6664 snapstate.Set(st, "alias-snap2", &snapstate.SnapState{ 6665 Sequence: []*snap.SideInfo{ 6666 {RealName: "alias-snap2", Revision: snap.R(12)}, 6667 }, 6668 Current: snap.R(12), 6669 Active: true, 6670 AutoAliasesDisabled: true, 6671 Aliases: map[string]*snapstate.AliasTarget{ 6672 "alias2": {Auto: "cmd2"}, 6673 "alias3": {Manual: "cmd3"}, 6674 "alias4": {Manual: "cmd4x", Auto: "cmd4"}, 6675 }, 6676 }) 6677 st.Unlock() 6678 6679 req, err := http.NewRequest("GET", "/v2/aliases", nil) 6680 c.Assert(err, check.IsNil) 6681 6682 rsp := getAliases(aliasesCmd, req, nil).(*resp) 6683 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 6684 c.Check(rsp.Status, check.Equals, 200) 6685 c.Check(rsp.Result, check.DeepEquals, map[string]map[string]aliasStatus{ 6686 "alias-snap1": { 6687 "alias1": { 6688 Command: "alias-snap1.cmd1x", 6689 Status: "manual", 6690 Manual: "cmd1x", 6691 Auto: "cmd1", 6692 }, 6693 "alias2": { 6694 Command: "alias-snap1.cmd2", 6695 Status: "auto", 6696 Auto: "cmd2", 6697 }, 6698 }, 6699 "alias-snap2": { 6700 "alias2": { 6701 Command: "alias-snap2.cmd2", 6702 Status: "disabled", 6703 Auto: "cmd2", 6704 }, 6705 "alias3": { 6706 Command: "alias-snap2.cmd3", 6707 Status: "manual", 6708 Manual: "cmd3", 6709 }, 6710 "alias4": { 6711 Command: "alias-snap2.cmd4x", 6712 Status: "manual", 6713 Manual: "cmd4x", 6714 Auto: "cmd4", 6715 }, 6716 }, 6717 }) 6718 6719 } 6720 6721 func (s *apiSuite) TestInstallUnaliased(c *check.C) { 6722 var calledFlags snapstate.Flags 6723 6724 snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { 6725 calledFlags = flags 6726 6727 t := s.NewTask("fake-install-snap", "Doing a fake install") 6728 return state.NewTaskSet(t), nil 6729 } 6730 6731 d := s.daemon(c) 6732 inst := &snapInstruction{ 6733 Action: "install", 6734 // Install the snap without enabled automatic aliases 6735 Unaliased: true, 6736 Snaps: []string{"fake"}, 6737 } 6738 6739 st := d.overlord.State() 6740 st.Lock() 6741 defer st.Unlock() 6742 _, _, err := inst.dispatch()(inst, st) 6743 c.Check(err, check.IsNil) 6744 6745 c.Check(calledFlags.Unaliased, check.Equals, true) 6746 } 6747 6748 func (s *apiSuite) TestInstallPathUnaliased(c *check.C) { 6749 body := "" + 6750 "----hello--\r\n" + 6751 "Content-Disposition: form-data; name=\"snap\"; filename=\"x\"\r\n" + 6752 "\r\n" + 6753 "xyzzy\r\n" + 6754 "----hello--\r\n" + 6755 "Content-Disposition: form-data; name=\"devmode\"\r\n" + 6756 "\r\n" + 6757 "true\r\n" + 6758 "----hello--\r\n" + 6759 "Content-Disposition: form-data; name=\"unaliased\"\r\n" + 6760 "\r\n" + 6761 "true\r\n" + 6762 "----hello--\r\n" 6763 head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} 6764 // try a multipart/form-data upload 6765 flags := snapstate.Flags{Unaliased: true, RemoveSnapPath: true, DevMode: true} 6766 chgSummary := s.sideloadCheck(c, body, head, "local", flags) 6767 c.Check(chgSummary, check.Equals, `Install "local" snap from file "x"`) 6768 } 6769 6770 func (s *apiSuite) TestSnapctlGetNoUID(c *check.C) { 6771 buf := bytes.NewBufferString(`{"context-id": "some-context", "args": ["get", "something"]}`) 6772 req, err := http.NewRequest("POST", "/v2/snapctl", buf) 6773 c.Assert(err, check.IsNil) 6774 rsp := runSnapctl(snapctlCmd, req, nil).(*resp) 6775 c.Assert(rsp.Status, check.Equals, 403) 6776 } 6777 6778 func (s *apiSuite) TestSnapctlForbiddenError(c *check.C) { 6779 _ = s.daemon(c) 6780 6781 runSnapctlUcrednetGet = func(string) (int32, uint32, string, error) { 6782 return 100, 9999, dirs.SnapSocket, nil 6783 } 6784 defer func() { runSnapctlUcrednetGet = ucrednetGet }() 6785 ctlcmdRun = func(ctx *hookstate.Context, arg []string, uid uint32) ([]byte, []byte, error) { 6786 return nil, nil, &ctlcmd.ForbiddenCommandError{} 6787 } 6788 defer func() { ctlcmdRun = ctlcmd.Run }() 6789 6790 buf := bytes.NewBufferString(fmt.Sprintf(`{"context-id": "some-context", "args": [%q, %q]}`, "set", "foo=bar")) 6791 req, err := http.NewRequest("POST", "/v2/snapctl", buf) 6792 c.Assert(err, check.IsNil) 6793 rsp := runSnapctl(snapctlCmd, req, nil).(*resp) 6794 c.Assert(rsp.Status, check.Equals, 403) 6795 } 6796 6797 func (s *apiSuite) TestSnapctlUnsuccesfulError(c *check.C) { 6798 _ = s.daemon(c) 6799 6800 runSnapctlUcrednetGet = func(string) (int32, uint32, string, error) { 6801 return 100, 9999, dirs.SnapSocket, nil 6802 } 6803 defer func() { runSnapctlUcrednetGet = ucrednetGet }() 6804 6805 ctlcmdRun = func(ctx *hookstate.Context, arg []string, uid uint32) ([]byte, []byte, error) { 6806 return nil, nil, &ctlcmd.UnsuccessfulError{ExitCode: 123} 6807 } 6808 defer func() { ctlcmdRun = ctlcmd.Run }() 6809 6810 buf := bytes.NewBufferString(fmt.Sprintf(`{"context-id": "some-context", "args": [%q, %q]}`, "is-connected", "plug")) 6811 req, err := http.NewRequest("POST", "/v2/snapctl", buf) 6812 c.Assert(err, check.IsNil) 6813 rsp := runSnapctl(snapctlCmd, req, nil).(*resp) 6814 c.Check(rsp.Status, check.Equals, 200) 6815 c.Check(rsp.Result.(*errorResult).Kind, check.Equals, client.ErrorKindUnsuccessful) 6816 c.Check(rsp.Result.(*errorResult).Value, check.DeepEquals, map[string]interface{}{ 6817 "stdout": "", 6818 "stderr": "", 6819 "exit-code": 123, 6820 }) 6821 } 6822 6823 type appSuite struct { 6824 apiBaseSuite 6825 cmd *testutil.MockCmd 6826 6827 infoA, infoB, infoC, infoD *snap.Info 6828 } 6829 6830 var _ = check.Suite(&appSuite{}) 6831 6832 func (s *appSuite) SetUpTest(c *check.C) { 6833 s.apiBaseSuite.SetUpTest(c) 6834 s.cmd = testutil.MockCommand(c, "systemctl", "").Also("journalctl", "") 6835 s.daemon(c) 6836 s.infoA = s.mkInstalledInState(c, s.d, "snap-a", "dev", "v1", snap.R(1), true, "apps: {svc1: {daemon: simple}, svc2: {daemon: simple, reload-command: x}}") 6837 s.infoB = s.mkInstalledInState(c, s.d, "snap-b", "dev", "v1", snap.R(1), false, "apps: {svc3: {daemon: simple}, cmd1: {}}") 6838 s.infoC = s.mkInstalledInState(c, s.d, "snap-c", "dev", "v1", snap.R(1), true, "") 6839 s.infoD = s.mkInstalledInState(c, s.d, "snap-d", "dev", "v1", snap.R(1), true, "apps: {cmd2: {}, cmd3: {}}") 6840 s.d.overlord.Loop() 6841 } 6842 6843 func (s *appSuite) TearDownTest(c *check.C) { 6844 s.d.overlord.Stop() 6845 s.cmd.Restore() 6846 s.apiBaseSuite.TearDownTest(c) 6847 } 6848 6849 func (s *appSuite) TestSplitAppName(c *check.C) { 6850 type T struct { 6851 name string 6852 snap string 6853 app string 6854 } 6855 6856 for _, x := range []T{ 6857 {name: "foo.bar", snap: "foo", app: "bar"}, 6858 {name: "foo", snap: "foo", app: ""}, 6859 {name: "foo.bar.baz", snap: "foo", app: "bar.baz"}, 6860 {name: ".", snap: "", app: ""}, // SISO 6861 } { 6862 snap, app := splitAppName(x.name) 6863 c.Check(x.snap, check.Equals, snap, check.Commentf(x.name)) 6864 c.Check(x.app, check.Equals, app, check.Commentf(x.name)) 6865 } 6866 } 6867 6868 func (s *appSuite) TestGetAppsInfo(c *check.C) { 6869 svcNames := []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"} 6870 for _, name := range svcNames { 6871 s.sysctlBufs = append(s.sysctlBufs, []byte(fmt.Sprintf(` 6872 Id=snap.%s.service 6873 Type=simple 6874 ActiveState=active 6875 UnitFileState=enabled 6876 `[1:], name))) 6877 } 6878 6879 req, err := http.NewRequest("GET", "/v2/apps", nil) 6880 c.Assert(err, check.IsNil) 6881 6882 rsp := getAppsInfo(appsCmd, req, nil).(*resp) 6883 c.Assert(rsp.Status, check.Equals, 200) 6884 c.Assert(rsp.Type, check.Equals, ResponseTypeSync) 6885 c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{}) 6886 apps := rsp.Result.([]client.AppInfo) 6887 c.Assert(apps, check.HasLen, 6) 6888 6889 for _, name := range svcNames { 6890 snap, app := splitAppName(name) 6891 needle := client.AppInfo{ 6892 Snap: snap, 6893 Name: app, 6894 Daemon: "simple", 6895 } 6896 if snap != "snap-b" { 6897 // snap-b is not active (all the others are) 6898 needle.Active = true 6899 needle.Enabled = true 6900 } 6901 c.Check(apps, testutil.DeepContains, needle) 6902 } 6903 6904 for _, name := range []string{"snap-b.cmd1", "snap-d.cmd2", "snap-d.cmd3"} { 6905 snap, app := splitAppName(name) 6906 c.Check(apps, testutil.DeepContains, client.AppInfo{ 6907 Snap: snap, 6908 Name: app, 6909 }) 6910 } 6911 6912 appNames := make([]string, len(apps)) 6913 for i, app := range apps { 6914 appNames[i] = app.Snap + "." + app.Name 6915 } 6916 c.Check(sort.StringsAreSorted(appNames), check.Equals, true) 6917 } 6918 6919 func (s *appSuite) TestGetAppsInfoNames(c *check.C) { 6920 6921 req, err := http.NewRequest("GET", "/v2/apps?names=snap-d", nil) 6922 c.Assert(err, check.IsNil) 6923 6924 rsp := getAppsInfo(appsCmd, req, nil).(*resp) 6925 c.Assert(rsp.Status, check.Equals, 200) 6926 c.Assert(rsp.Type, check.Equals, ResponseTypeSync) 6927 c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{}) 6928 apps := rsp.Result.([]client.AppInfo) 6929 c.Assert(apps, check.HasLen, 2) 6930 6931 for _, name := range []string{"snap-d.cmd2", "snap-d.cmd3"} { 6932 snap, app := splitAppName(name) 6933 c.Check(apps, testutil.DeepContains, client.AppInfo{ 6934 Snap: snap, 6935 Name: app, 6936 }) 6937 } 6938 6939 appNames := make([]string, len(apps)) 6940 for i, app := range apps { 6941 appNames[i] = app.Snap + "." + app.Name 6942 } 6943 c.Check(sort.StringsAreSorted(appNames), check.Equals, true) 6944 } 6945 6946 func (s *appSuite) TestGetAppsInfoServices(c *check.C) { 6947 svcNames := []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"} 6948 for _, name := range svcNames { 6949 s.sysctlBufs = append(s.sysctlBufs, []byte(fmt.Sprintf(` 6950 Id=snap.%s.service 6951 Type=simple 6952 ActiveState=active 6953 UnitFileState=enabled 6954 `[1:], name))) 6955 } 6956 6957 req, err := http.NewRequest("GET", "/v2/apps?select=service", nil) 6958 c.Assert(err, check.IsNil) 6959 6960 rsp := getAppsInfo(appsCmd, req, nil).(*resp) 6961 c.Assert(rsp.Status, check.Equals, 200) 6962 c.Assert(rsp.Type, check.Equals, ResponseTypeSync) 6963 c.Assert(rsp.Result, check.FitsTypeOf, []client.AppInfo{}) 6964 svcs := rsp.Result.([]client.AppInfo) 6965 c.Assert(svcs, check.HasLen, 3) 6966 6967 for _, name := range svcNames { 6968 snap, app := splitAppName(name) 6969 needle := client.AppInfo{ 6970 Snap: snap, 6971 Name: app, 6972 Daemon: "simple", 6973 } 6974 if snap != "snap-b" { 6975 // snap-b is not active (all the others are) 6976 needle.Active = true 6977 needle.Enabled = true 6978 } 6979 c.Check(svcs, testutil.DeepContains, needle) 6980 } 6981 6982 appNames := make([]string, len(svcs)) 6983 for i, svc := range svcs { 6984 appNames[i] = svc.Snap + "." + svc.Name 6985 } 6986 c.Check(sort.StringsAreSorted(appNames), check.Equals, true) 6987 } 6988 6989 func (s *appSuite) TestGetAppsInfoBadSelect(c *check.C) { 6990 req, err := http.NewRequest("GET", "/v2/apps?select=potato", nil) 6991 c.Assert(err, check.IsNil) 6992 6993 rsp := getAppsInfo(appsCmd, req, nil).(*resp) 6994 c.Assert(rsp.Status, check.Equals, 400) 6995 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 6996 } 6997 6998 func (s *appSuite) TestGetAppsInfoBadName(c *check.C) { 6999 req, err := http.NewRequest("GET", "/v2/apps?names=potato", nil) 7000 c.Assert(err, check.IsNil) 7001 7002 rsp := getAppsInfo(appsCmd, req, nil).(*resp) 7003 c.Assert(rsp.Status, check.Equals, 404) 7004 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 7005 } 7006 7007 func (s *appSuite) TestAppInfosForOne(c *check.C) { 7008 st := s.d.overlord.State() 7009 appInfos, rsp := appInfosFor(st, []string{"snap-a.svc1"}, appInfoOptions{service: true}) 7010 c.Assert(rsp, check.IsNil) 7011 c.Assert(appInfos, check.HasLen, 1) 7012 c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) 7013 c.Check(appInfos[0].Name, check.Equals, "svc1") 7014 } 7015 7016 func (s *appSuite) TestAppInfosForAll(c *check.C) { 7017 type T struct { 7018 opts appInfoOptions 7019 snaps []*snap.Info 7020 names []string 7021 } 7022 7023 for _, t := range []T{ 7024 { 7025 opts: appInfoOptions{service: true}, 7026 names: []string{"svc1", "svc2", "svc3"}, 7027 snaps: []*snap.Info{s.infoA, s.infoA, s.infoB}, 7028 }, 7029 { 7030 opts: appInfoOptions{}, 7031 names: []string{"svc1", "svc2", "cmd1", "svc3", "cmd2", "cmd3"}, 7032 snaps: []*snap.Info{s.infoA, s.infoA, s.infoB, s.infoB, s.infoD, s.infoD}, 7033 }, 7034 } { 7035 c.Assert(len(t.names), check.Equals, len(t.snaps), check.Commentf("%s", t.opts)) 7036 7037 st := s.d.overlord.State() 7038 appInfos, rsp := appInfosFor(st, nil, t.opts) 7039 c.Assert(rsp, check.IsNil, check.Commentf("%s", t.opts)) 7040 names := make([]string, len(appInfos)) 7041 for i, appInfo := range appInfos { 7042 names[i] = appInfo.Name 7043 } 7044 c.Assert(names, check.DeepEquals, t.names, check.Commentf("%s", t.opts)) 7045 7046 for i := range appInfos { 7047 c.Check(appInfos[i].Snap, check.DeepEquals, t.snaps[i], check.Commentf("%s: %s", t.opts, t.names[i])) 7048 } 7049 } 7050 } 7051 7052 func (s *appSuite) TestAppInfosForOneSnap(c *check.C) { 7053 st := s.d.overlord.State() 7054 appInfos, rsp := appInfosFor(st, []string{"snap-a"}, appInfoOptions{service: true}) 7055 c.Assert(rsp, check.IsNil) 7056 c.Assert(appInfos, check.HasLen, 2) 7057 sort.Sort(snap.AppInfoBySnapApp(appInfos)) 7058 7059 c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) 7060 c.Check(appInfos[0].Name, check.Equals, "svc1") 7061 c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA) 7062 c.Check(appInfos[1].Name, check.Equals, "svc2") 7063 } 7064 7065 func (s *appSuite) TestAppInfosForMixedArgs(c *check.C) { 7066 st := s.d.overlord.State() 7067 appInfos, rsp := appInfosFor(st, []string{"snap-a", "snap-a.svc1"}, appInfoOptions{service: true}) 7068 c.Assert(rsp, check.IsNil) 7069 c.Assert(appInfos, check.HasLen, 2) 7070 sort.Sort(snap.AppInfoBySnapApp(appInfos)) 7071 7072 c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) 7073 c.Check(appInfos[0].Name, check.Equals, "svc1") 7074 c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA) 7075 c.Check(appInfos[1].Name, check.Equals, "svc2") 7076 } 7077 7078 func (s *appSuite) TestAppInfosCleanupAndSorted(c *check.C) { 7079 st := s.d.overlord.State() 7080 appInfos, rsp := appInfosFor(st, []string{ 7081 "snap-b.svc3", 7082 "snap-a.svc2", 7083 "snap-a.svc1", 7084 "snap-a.svc2", 7085 "snap-b.svc3", 7086 "snap-a.svc1", 7087 "snap-b", 7088 "snap-a", 7089 }, appInfoOptions{service: true}) 7090 c.Assert(rsp, check.IsNil) 7091 c.Assert(appInfos, check.HasLen, 3) 7092 sort.Sort(snap.AppInfoBySnapApp(appInfos)) 7093 7094 c.Check(appInfos[0].Snap, check.DeepEquals, s.infoA) 7095 c.Check(appInfos[0].Name, check.Equals, "svc1") 7096 c.Check(appInfos[1].Snap, check.DeepEquals, s.infoA) 7097 c.Check(appInfos[1].Name, check.Equals, "svc2") 7098 c.Check(appInfos[2].Snap, check.DeepEquals, s.infoB) 7099 c.Check(appInfos[2].Name, check.Equals, "svc3") 7100 } 7101 7102 func (s *appSuite) TestAppInfosForAppless(c *check.C) { 7103 st := s.d.overlord.State() 7104 appInfos, rsp := appInfosFor(st, []string{"snap-c"}, appInfoOptions{service: true}) 7105 c.Assert(rsp, check.FitsTypeOf, &resp{}) 7106 c.Check(rsp.(*resp).Status, check.Equals, 404) 7107 c.Check(rsp.(*resp).Result.(*errorResult).Kind, check.Equals, client.ErrorKindAppNotFound) 7108 c.Assert(appInfos, check.IsNil) 7109 } 7110 7111 func (s *appSuite) TestAppInfosForMissingApp(c *check.C) { 7112 st := s.d.overlord.State() 7113 appInfos, rsp := appInfosFor(st, []string{"snap-c.whatever"}, appInfoOptions{service: true}) 7114 c.Assert(rsp, check.FitsTypeOf, &resp{}) 7115 c.Check(rsp.(*resp).Status, check.Equals, 404) 7116 c.Check(rsp.(*resp).Result.(*errorResult).Kind, check.Equals, client.ErrorKindAppNotFound) 7117 c.Assert(appInfos, check.IsNil) 7118 } 7119 7120 func (s *appSuite) TestAppInfosForMissingSnap(c *check.C) { 7121 st := s.d.overlord.State() 7122 appInfos, rsp := appInfosFor(st, []string{"snap-x"}, appInfoOptions{service: true}) 7123 c.Assert(rsp, check.FitsTypeOf, &resp{}) 7124 c.Check(rsp.(*resp).Status, check.Equals, 404) 7125 c.Check(rsp.(*resp).Result.(*errorResult).Kind, check.Equals, client.ErrorKindSnapNotFound) 7126 c.Assert(appInfos, check.IsNil) 7127 } 7128 7129 func (s *apiSuite) TestLogsNoServices(c *check.C) { 7130 // NOTE this is *apiSuite, not *appSuite, so there are no 7131 // installed snaps with services 7132 7133 cmd := testutil.MockCommand(c, "systemctl", "").Also("journalctl", "") 7134 defer cmd.Restore() 7135 s.daemon(c) 7136 s.d.overlord.Loop() 7137 defer s.d.overlord.Stop() 7138 7139 req, err := http.NewRequest("GET", "/v2/logs", nil) 7140 c.Assert(err, check.IsNil) 7141 7142 rsp := getLogs(logsCmd, req, nil).(*resp) 7143 c.Assert(rsp.Status, check.Equals, 404) 7144 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 7145 } 7146 7147 func (s *appSuite) TestLogs(c *check.C) { 7148 s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(` 7149 {"MESSAGE": "hello1", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "42"} 7150 {"MESSAGE": "hello2", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "44"} 7151 {"MESSAGE": "hello3", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "46"} 7152 {"MESSAGE": "hello4", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "48"} 7153 {"MESSAGE": "hello5", "SYSLOG_IDENTIFIER": "xyzzy", "_PID": "42", "__REALTIME_TIMESTAMP": "50"} 7154 `))} 7155 7156 req, err := http.NewRequest("GET", "/v2/logs?names=snap-a.svc2&n=42&follow=false", nil) 7157 c.Assert(err, check.IsNil) 7158 7159 rec := httptest.NewRecorder() 7160 getLogs(logsCmd, req, nil).ServeHTTP(rec, req) 7161 7162 c.Check(s.jctlSvcses, check.DeepEquals, [][]string{{"snap.snap-a.svc2.service"}}) 7163 c.Check(s.jctlNs, check.DeepEquals, []int{42}) 7164 c.Check(s.jctlFollows, check.DeepEquals, []bool{false}) 7165 7166 c.Check(rec.Code, check.Equals, 200) 7167 c.Check(rec.HeaderMap.Get("Content-Type"), check.Equals, "application/json-seq") 7168 c.Check(rec.Body.String(), check.Equals, ` 7169 {"timestamp":"1970-01-01T00:00:00.000042Z","message":"hello1","sid":"xyzzy","pid":"42"} 7170 {"timestamp":"1970-01-01T00:00:00.000044Z","message":"hello2","sid":"xyzzy","pid":"42"} 7171 {"timestamp":"1970-01-01T00:00:00.000046Z","message":"hello3","sid":"xyzzy","pid":"42"} 7172 {"timestamp":"1970-01-01T00:00:00.000048Z","message":"hello4","sid":"xyzzy","pid":"42"} 7173 {"timestamp":"1970-01-01T00:00:00.00005Z","message":"hello5","sid":"xyzzy","pid":"42"} 7174 `[1:]) 7175 } 7176 7177 func (s *appSuite) TestLogsN(c *check.C) { 7178 type T struct { 7179 in string 7180 out int 7181 } 7182 7183 for _, t := range []T{ 7184 {in: "", out: 10}, 7185 {in: "0", out: 0}, 7186 {in: "-1", out: -1}, 7187 {in: strconv.Itoa(math.MinInt32), out: math.MinInt32}, 7188 {in: strconv.Itoa(math.MaxInt32), out: math.MaxInt32}, 7189 } { 7190 7191 s.jctlRCs = []io.ReadCloser{ioutil.NopCloser(strings.NewReader(""))} 7192 s.jctlNs = nil 7193 7194 req, err := http.NewRequest("GET", "/v2/logs?n="+t.in, nil) 7195 c.Assert(err, check.IsNil) 7196 7197 rec := httptest.NewRecorder() 7198 getLogs(logsCmd, req, nil).ServeHTTP(rec, req) 7199 7200 c.Check(s.jctlNs, check.DeepEquals, []int{t.out}) 7201 } 7202 } 7203 7204 func (s *appSuite) TestLogsBadN(c *check.C) { 7205 req, err := http.NewRequest("GET", "/v2/logs?n=hello", nil) 7206 c.Assert(err, check.IsNil) 7207 7208 rsp := getLogs(logsCmd, req, nil).(*resp) 7209 c.Assert(rsp.Status, check.Equals, 400) 7210 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 7211 } 7212 7213 func (s *appSuite) TestLogsFollow(c *check.C) { 7214 s.jctlRCs = []io.ReadCloser{ 7215 ioutil.NopCloser(strings.NewReader("")), 7216 ioutil.NopCloser(strings.NewReader("")), 7217 ioutil.NopCloser(strings.NewReader("")), 7218 } 7219 7220 reqT, err := http.NewRequest("GET", "/v2/logs?follow=true", nil) 7221 c.Assert(err, check.IsNil) 7222 reqF, err := http.NewRequest("GET", "/v2/logs?follow=false", nil) 7223 c.Assert(err, check.IsNil) 7224 reqN, err := http.NewRequest("GET", "/v2/logs", nil) 7225 c.Assert(err, check.IsNil) 7226 7227 rec := httptest.NewRecorder() 7228 getLogs(logsCmd, reqT, nil).ServeHTTP(rec, reqT) 7229 getLogs(logsCmd, reqF, nil).ServeHTTP(rec, reqF) 7230 getLogs(logsCmd, reqN, nil).ServeHTTP(rec, reqN) 7231 7232 c.Check(s.jctlFollows, check.DeepEquals, []bool{true, false, false}) 7233 } 7234 7235 func (s *appSuite) TestLogsBadFollow(c *check.C) { 7236 req, err := http.NewRequest("GET", "/v2/logs?follow=hello", nil) 7237 c.Assert(err, check.IsNil) 7238 7239 rsp := getLogs(logsCmd, req, nil).(*resp) 7240 c.Assert(rsp.Status, check.Equals, 400) 7241 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 7242 } 7243 7244 func (s *appSuite) TestLogsBadName(c *check.C) { 7245 req, err := http.NewRequest("GET", "/v2/logs?names=hello", nil) 7246 c.Assert(err, check.IsNil) 7247 7248 rsp := getLogs(logsCmd, req, nil).(*resp) 7249 c.Assert(rsp.Status, check.Equals, 404) 7250 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 7251 } 7252 7253 func (s *appSuite) TestLogsSad(c *check.C) { 7254 s.jctlErrs = []error{errors.New("potato")} 7255 req, err := http.NewRequest("GET", "/v2/logs", nil) 7256 c.Assert(err, check.IsNil) 7257 7258 rsp := getLogs(logsCmd, req, nil).(*resp) 7259 c.Assert(rsp.Status, check.Equals, 500) 7260 c.Assert(rsp.Type, check.Equals, ResponseTypeError) 7261 } 7262 7263 func (s *appSuite) testPostApps(c *check.C, inst servicestate.Instruction, servicecmds []serviceControlArgs) *state.Change { 7264 postBody, err := json.Marshal(inst) 7265 c.Assert(err, check.IsNil) 7266 7267 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBuffer(postBody)) 7268 c.Assert(err, check.IsNil) 7269 7270 rsp := postApps(appsCmd, req, nil).(*resp) 7271 c.Assert(rsp.Status, check.Equals, 202) 7272 c.Assert(rsp.Type, check.Equals, ResponseTypeAsync) 7273 c.Check(rsp.Change, check.Matches, `[0-9]+`) 7274 7275 st := s.d.overlord.State() 7276 st.Lock() 7277 defer st.Unlock() 7278 chg := st.Change(rsp.Change) 7279 c.Assert(chg, check.NotNil) 7280 c.Check(chg.Tasks(), check.HasLen, len(servicecmds)) 7281 7282 st.Unlock() 7283 <-chg.Ready() 7284 st.Lock() 7285 7286 c.Check(s.serviceControlCalls, check.DeepEquals, servicecmds) 7287 return chg 7288 } 7289 7290 func (s *appSuite) TestPostAppsStartOne(c *check.C) { 7291 inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}} 7292 expected := []serviceControlArgs{ 7293 {action: "start", names: []string{"snap-a.svc2"}}, 7294 } 7295 chg := s.testPostApps(c, inst, expected) 7296 chg.State().Lock() 7297 defer chg.State().Unlock() 7298 7299 var names []string 7300 err := chg.Get("snap-names", &names) 7301 c.Assert(err, check.IsNil) 7302 c.Assert(names, check.DeepEquals, []string{"snap-a"}) 7303 } 7304 7305 func (s *appSuite) TestPostAppsStartTwo(c *check.C) { 7306 inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a"}} 7307 expected := []serviceControlArgs{ 7308 {action: "start", names: []string{"snap-a.svc1", "snap-a.svc2"}}, 7309 } 7310 chg := s.testPostApps(c, inst, expected) 7311 7312 chg.State().Lock() 7313 defer chg.State().Unlock() 7314 7315 var names []string 7316 err := chg.Get("snap-names", &names) 7317 c.Assert(err, check.IsNil) 7318 c.Assert(names, check.DeepEquals, []string{"snap-a"}) 7319 } 7320 7321 func (s *appSuite) TestPostAppsStartThree(c *check.C) { 7322 inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a", "snap-b"}} 7323 expected := []serviceControlArgs{ 7324 {action: "start", names: []string{"snap-a.svc1", "snap-a.svc2", "snap-b.svc3"}}, 7325 } 7326 chg := s.testPostApps(c, inst, expected) 7327 // check the summary expands the snap into actual apps 7328 c.Check(chg.Summary(), check.Equals, "Running service command") 7329 chg.State().Lock() 7330 defer chg.State().Unlock() 7331 7332 var names []string 7333 err := chg.Get("snap-names", &names) 7334 c.Assert(err, check.IsNil) 7335 c.Assert(names, check.DeepEquals, []string{"snap-a", "snap-b"}) 7336 } 7337 7338 func (s *appSuite) TestPostAppsStop(c *check.C) { 7339 inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}} 7340 expected := []serviceControlArgs{ 7341 {action: "stop", names: []string{"snap-a.svc2"}}, 7342 } 7343 s.testPostApps(c, inst, expected) 7344 } 7345 7346 func (s *appSuite) TestPostAppsRestart(c *check.C) { 7347 inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}} 7348 expected := []serviceControlArgs{ 7349 {action: "restart", names: []string{"snap-a.svc2"}}, 7350 } 7351 s.testPostApps(c, inst, expected) 7352 } 7353 7354 func (s *appSuite) TestPostAppsReload(c *check.C) { 7355 inst := servicestate.Instruction{Action: "restart", Names: []string{"snap-a.svc2"}} 7356 inst.Reload = true 7357 expected := []serviceControlArgs{ 7358 {action: "restart", options: "reload", names: []string{"snap-a.svc2"}}, 7359 } 7360 s.testPostApps(c, inst, expected) 7361 } 7362 7363 func (s *appSuite) TestPostAppsEnableNow(c *check.C) { 7364 inst := servicestate.Instruction{Action: "start", Names: []string{"snap-a.svc2"}} 7365 inst.Enable = true 7366 expected := []serviceControlArgs{ 7367 {action: "start", options: "enable", names: []string{"snap-a.svc2"}}, 7368 } 7369 s.testPostApps(c, inst, expected) 7370 } 7371 7372 func (s *appSuite) TestPostAppsDisableNow(c *check.C) { 7373 inst := servicestate.Instruction{Action: "stop", Names: []string{"snap-a.svc2"}} 7374 inst.Disable = true 7375 expected := []serviceControlArgs{ 7376 {action: "stop", options: "disable", names: []string{"snap-a.svc2"}}, 7377 } 7378 s.testPostApps(c, inst, expected) 7379 } 7380 7381 func (s *appSuite) TestPostAppsBadJSON(c *check.C) { 7382 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`'junk`)) 7383 c.Assert(err, check.IsNil) 7384 rsp := postApps(appsCmd, req, nil).(*resp) 7385 c.Check(rsp.Status, check.Equals, 400) 7386 c.Check(rsp.Type, check.Equals, ResponseTypeError) 7387 c.Check(rsp.Result.(*errorResult).Message, check.Matches, ".*cannot decode request body.*") 7388 } 7389 7390 func (s *appSuite) TestPostAppsBadOp(c *check.C) { 7391 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"random": "json"}`)) 7392 c.Assert(err, check.IsNil) 7393 rsp := postApps(appsCmd, req, nil).(*resp) 7394 c.Check(rsp.Status, check.Equals, 400) 7395 c.Check(rsp.Type, check.Equals, ResponseTypeError) 7396 c.Check(rsp.Result.(*errorResult).Message, check.Matches, ".*cannot perform operation on services without a list of services.*") 7397 } 7398 7399 func (s *appSuite) TestPostAppsBadSnap(c *check.C) { 7400 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-c"]}`)) 7401 c.Assert(err, check.IsNil) 7402 rsp := postApps(appsCmd, req, nil).(*resp) 7403 c.Check(rsp.Status, check.Equals, 404) 7404 c.Check(rsp.Type, check.Equals, ResponseTypeError) 7405 c.Check(rsp.Result.(*errorResult).Message, check.Equals, `snap "snap-c" has no services`) 7406 } 7407 7408 func (s *appSuite) TestPostAppsBadApp(c *check.C) { 7409 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "stop", "names": ["snap-a.what"]}`)) 7410 c.Assert(err, check.IsNil) 7411 rsp := postApps(appsCmd, req, nil).(*resp) 7412 c.Check(rsp.Status, check.Equals, 404) 7413 c.Check(rsp.Type, check.Equals, ResponseTypeError) 7414 c.Check(rsp.Result.(*errorResult).Message, check.Equals, `snap "snap-a" has no service "what"`) 7415 } 7416 7417 func (s *appSuite) TestPostAppsServiceControlError(c *check.C) { 7418 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`)) 7419 c.Assert(err, check.IsNil) 7420 s.serviceControlError = fmt.Errorf("total failure") 7421 rsp := postApps(appsCmd, req, nil).(*resp) 7422 c.Check(rsp.Status, check.Equals, 400) 7423 c.Check(rsp.Type, check.Equals, ResponseTypeError) 7424 c.Check(rsp.Result.(*errorResult).Message, check.Equals, `total failure`) 7425 } 7426 7427 func (s *appSuite) TestPostAppsConflict(c *check.C) { 7428 req, err := http.NewRequest("POST", "/v2/apps", bytes.NewBufferString(`{"action": "start", "names": ["snap-a.svc1"]}`)) 7429 c.Assert(err, check.IsNil) 7430 s.serviceControlError = &snapstate.ChangeConflictError{Snap: "snap-a", ChangeKind: "enable"} 7431 rsp := postApps(appsCmd, req, nil).(*resp) 7432 c.Check(rsp.Status, check.Equals, 400) 7433 c.Check(rsp.Type, check.Equals, ResponseTypeError) 7434 c.Check(rsp.Result.(*errorResult).Message, check.Equals, `snap "snap-a" has "enable" change in progress`) 7435 } 7436 7437 type fakeNetError struct { 7438 message string 7439 timeout bool 7440 temporary bool 7441 } 7442 7443 func (e fakeNetError) Error() string { return e.message } 7444 func (e fakeNetError) Timeout() bool { return e.timeout } 7445 func (e fakeNetError) Temporary() bool { return e.temporary } 7446 7447 func (s *apiSuite) TestErrToResponseNoSnapsDoesNotPanic(c *check.C) { 7448 si := &snapInstruction{Action: "frobble"} 7449 errors := []error{ 7450 store.ErrSnapNotFound, 7451 &store.RevisionNotAvailableError{}, 7452 store.ErrNoUpdateAvailable, 7453 store.ErrLocalSnap, 7454 &snap.AlreadyInstalledError{Snap: "foo"}, 7455 &snap.NotInstalledError{Snap: "foo"}, 7456 &snapstate.SnapNeedsDevModeError{Snap: "foo"}, 7457 &snapstate.SnapNeedsClassicError{Snap: "foo"}, 7458 &snapstate.SnapNeedsClassicSystemError{Snap: "foo"}, 7459 fakeNetError{message: "other"}, 7460 fakeNetError{message: "timeout", timeout: true}, 7461 fakeNetError{message: "temp", temporary: true}, 7462 errors.New("some other error"), 7463 } 7464 7465 for _, err := range errors { 7466 rsp := si.errToResponse(err) 7467 com := check.Commentf("%v", err) 7468 c.Check(rsp, check.NotNil, com) 7469 status := rsp.(*resp).Status 7470 c.Check(status/100 == 4 || status/100 == 5, check.Equals, true, com) 7471 } 7472 } 7473 7474 func (s *apiSuite) TestErrToResponseForRevisionNotAvailable(c *check.C) { 7475 si := &snapInstruction{Action: "frobble", Snaps: []string{"foo"}} 7476 7477 thisArch := arch.DpkgArchitecture() 7478 7479 err := &store.RevisionNotAvailableError{ 7480 Action: "install", 7481 Channel: "stable", 7482 Releases: []channel.Channel{ 7483 snaptest.MustParseChannel("beta", thisArch), 7484 }, 7485 } 7486 rsp := si.errToResponse(err).(*resp) 7487 c.Check(rsp, check.DeepEquals, &resp{ 7488 Status: 404, 7489 Type: ResponseTypeError, 7490 Result: &errorResult{ 7491 Message: "no snap revision on specified channel", 7492 Kind: client.ErrorKindSnapChannelNotAvailable, 7493 Value: map[string]interface{}{ 7494 "snap-name": "foo", 7495 "action": "install", 7496 "channel": "stable", 7497 "architecture": thisArch, 7498 "releases": []map[string]interface{}{ 7499 {"architecture": thisArch, "channel": "beta"}, 7500 }, 7501 }, 7502 }, 7503 }) 7504 7505 err = &store.RevisionNotAvailableError{ 7506 Action: "install", 7507 Channel: "stable", 7508 Releases: []channel.Channel{ 7509 snaptest.MustParseChannel("beta", "other-arch"), 7510 }, 7511 } 7512 rsp = si.errToResponse(err).(*resp) 7513 c.Check(rsp, check.DeepEquals, &resp{ 7514 Status: 404, 7515 Type: ResponseTypeError, 7516 Result: &errorResult{ 7517 Message: "no snap revision on specified architecture", 7518 Kind: client.ErrorKindSnapArchitectureNotAvailable, 7519 Value: map[string]interface{}{ 7520 "snap-name": "foo", 7521 "action": "install", 7522 "channel": "stable", 7523 "architecture": thisArch, 7524 "releases": []map[string]interface{}{ 7525 {"architecture": "other-arch", "channel": "beta"}, 7526 }, 7527 }, 7528 }, 7529 }) 7530 7531 err = &store.RevisionNotAvailableError{} 7532 rsp = si.errToResponse(err).(*resp) 7533 c.Check(rsp, check.DeepEquals, &resp{ 7534 Status: 404, 7535 Type: ResponseTypeError, 7536 Result: &errorResult{ 7537 Message: "no snap revision available as specified", 7538 Kind: client.ErrorKindSnapRevisionNotAvailable, 7539 Value: "foo", 7540 }, 7541 }) 7542 } 7543 7544 func (s *apiSuite) testWarnings(c *check.C, all bool, body io.Reader) (calls string, result interface{}) { 7545 s.daemon(c) 7546 7547 oldOK := stateOkayWarnings 7548 oldAll := stateAllWarnings 7549 oldPending := statePendingWarnings 7550 stateOkayWarnings = func(*state.State, time.Time) int { calls += "ok"; return 0 } 7551 stateAllWarnings = func(*state.State) []*state.Warning { calls += "all"; return nil } 7552 statePendingWarnings = func(*state.State) ([]*state.Warning, time.Time) { calls += "show"; return nil, time.Time{} } 7553 defer func() { 7554 stateOkayWarnings = oldOK 7555 stateAllWarnings = oldAll 7556 statePendingWarnings = oldPending 7557 }() 7558 7559 method := "GET" 7560 f := warningsCmd.GET 7561 if body != nil { 7562 method = "POST" 7563 f = warningsCmd.POST 7564 } 7565 q := url.Values{} 7566 if all { 7567 q.Set("select", "all") 7568 } 7569 req, err := http.NewRequest(method, "/v2/warnings?"+q.Encode(), body) 7570 c.Assert(err, check.IsNil) 7571 7572 rsp, ok := f(warningsCmd, req, nil).(*resp) 7573 c.Assert(ok, check.Equals, true) 7574 7575 c.Check(rsp.Type, check.Equals, ResponseTypeSync) 7576 c.Check(rsp.Status, check.Equals, 200) 7577 c.Assert(rsp.Result, check.NotNil) 7578 return calls, rsp.Result 7579 } 7580 7581 func (s *apiSuite) TestAllWarnings(c *check.C) { 7582 calls, result := s.testWarnings(c, true, nil) 7583 c.Check(calls, check.Equals, "all") 7584 c.Check(result, check.DeepEquals, []state.Warning{}) 7585 } 7586 7587 func (s *apiSuite) TestSomeWarnings(c *check.C) { 7588 calls, result := s.testWarnings(c, false, nil) 7589 c.Check(calls, check.Equals, "show") 7590 c.Check(result, check.DeepEquals, []state.Warning{}) 7591 } 7592 7593 func (s *apiSuite) TestAckWarnings(c *check.C) { 7594 calls, result := s.testWarnings(c, false, bytes.NewReader([]byte(`{"action": "okay", "timestamp": "2006-01-02T15:04:05Z"}`))) 7595 c.Check(calls, check.Equals, "ok") 7596 c.Check(result, check.DeepEquals, 0) 7597 } 7598 7599 func (s *apiSuite) TestErrToResponseForChangeConflict(c *check.C) { 7600 si := &snapInstruction{Action: "frobble", Snaps: []string{"foo"}} 7601 7602 err := &snapstate.ChangeConflictError{Snap: "foo", ChangeKind: "install"} 7603 rsp := si.errToResponse(err).(*resp) 7604 c.Check(rsp, check.DeepEquals, &resp{ 7605 Status: 409, 7606 Type: ResponseTypeError, 7607 Result: &errorResult{ 7608 Message: `snap "foo" has "install" change in progress`, 7609 Kind: client.ErrorKindSnapChangeConflict, 7610 Value: map[string]interface{}{ 7611 "snap-name": "foo", 7612 "change-kind": "install", 7613 }, 7614 }, 7615 }) 7616 7617 // only snap 7618 err = &snapstate.ChangeConflictError{Snap: "foo"} 7619 rsp = si.errToResponse(err).(*resp) 7620 c.Check(rsp, check.DeepEquals, &resp{ 7621 Status: 409, 7622 Type: ResponseTypeError, 7623 Result: &errorResult{ 7624 Message: `snap "foo" has changes in progress`, 7625 Kind: client.ErrorKindSnapChangeConflict, 7626 Value: map[string]interface{}{ 7627 "snap-name": "foo", 7628 }, 7629 }, 7630 }) 7631 7632 // only kind 7633 err = &snapstate.ChangeConflictError{Message: "specific error msg", ChangeKind: "some-global-op"} 7634 rsp = si.errToResponse(err).(*resp) 7635 c.Check(rsp, check.DeepEquals, &resp{ 7636 Status: 409, 7637 Type: ResponseTypeError, 7638 Result: &errorResult{ 7639 Message: "specific error msg", 7640 Kind: client.ErrorKindSnapChangeConflict, 7641 Value: map[string]interface{}{ 7642 "change-kind": "some-global-op", 7643 }, 7644 }, 7645 }) 7646 } 7647 7648 func (s *apiSuite) TestErrToResponseInsufficentSpace(c *check.C) { 7649 err := &snapstate.InsufficientSpaceError{ 7650 Snaps: []string{"foo", "bar"}, 7651 ChangeKind: "some-change", 7652 Path: "/path", 7653 Message: "specific error msg", 7654 } 7655 rsp := errToResponse(err, nil, BadRequest, "%s: %v", "ERR").(*resp) 7656 c.Check(rsp, check.DeepEquals, &resp{ 7657 Status: 507, 7658 Type: ResponseTypeError, 7659 Result: &errorResult{ 7660 Message: "specific error msg", 7661 Kind: client.ErrorKindInsufficientDiskSpace, 7662 Value: map[string]interface{}{ 7663 "snap-names": []string{"foo", "bar"}, 7664 "change-kind": "some-change", 7665 }, 7666 }, 7667 }) 7668 } 7669 7670 func (s *apiSuite) TestErrToResponse(c *check.C) { 7671 aie := &snap.AlreadyInstalledError{Snap: "foo"} 7672 nie := &snap.NotInstalledError{Snap: "foo"} 7673 cce := &snapstate.ChangeConflictError{Snap: "foo"} 7674 ndme := &snapstate.SnapNeedsDevModeError{Snap: "foo"} 7675 nc := &snapstate.SnapNotClassicError{Snap: "foo"} 7676 nce := &snapstate.SnapNeedsClassicError{Snap: "foo"} 7677 ncse := &snapstate.SnapNeedsClassicSystemError{Snap: "foo"} 7678 netoe := fakeNetError{message: "other"} 7679 nettoute := fakeNetError{message: "timeout", timeout: true} 7680 nettmpe := fakeNetError{message: "temp", temporary: true} 7681 7682 e := errors.New("other error") 7683 7684 sa1e := &store.SnapActionError{Refresh: map[string]error{"foo": store.ErrSnapNotFound}} 7685 sa2e := &store.SnapActionError{Refresh: map[string]error{ 7686 "foo": store.ErrSnapNotFound, 7687 "bar": store.ErrSnapNotFound, 7688 }} 7689 saOe := &store.SnapActionError{Other: []error{e}} 7690 // this one can't happen (but fun to test): 7691 saXe := &store.SnapActionError{Refresh: map[string]error{"foo": sa1e}} 7692 7693 makeErrorRsp := func(kind client.ErrorKind, err error, value interface{}) Response { 7694 return SyncResponse(&resp{ 7695 Type: ResponseTypeError, 7696 Result: &errorResult{Message: err.Error(), Kind: kind, Value: value}, 7697 Status: 400, 7698 }, nil) 7699 } 7700 7701 tests := []struct { 7702 err error 7703 expectedRsp Response 7704 }{ 7705 {store.ErrSnapNotFound, SnapNotFound("foo", store.ErrSnapNotFound)}, 7706 {store.ErrNoUpdateAvailable, makeErrorRsp(client.ErrorKindSnapNoUpdateAvailable, store.ErrNoUpdateAvailable, "")}, 7707 {store.ErrLocalSnap, makeErrorRsp(client.ErrorKindSnapLocal, store.ErrLocalSnap, "")}, 7708 {aie, makeErrorRsp(client.ErrorKindSnapAlreadyInstalled, aie, "foo")}, 7709 {nie, makeErrorRsp(client.ErrorKindSnapNotInstalled, nie, "foo")}, 7710 {ndme, makeErrorRsp(client.ErrorKindSnapNeedsDevMode, ndme, "foo")}, 7711 {nc, makeErrorRsp(client.ErrorKindSnapNotClassic, nc, "foo")}, 7712 {nce, makeErrorRsp(client.ErrorKindSnapNeedsClassic, nce, "foo")}, 7713 {ncse, makeErrorRsp(client.ErrorKindSnapNeedsClassicSystem, ncse, "foo")}, 7714 {cce, SnapChangeConflict(cce)}, 7715 {nettoute, makeErrorRsp(client.ErrorKindNetworkTimeout, nettoute, "")}, 7716 {netoe, BadRequest("ERR: %v", netoe)}, 7717 {nettmpe, BadRequest("ERR: %v", nettmpe)}, 7718 {e, BadRequest("ERR: %v", e)}, 7719 7720 // action error unwrapping: 7721 {sa1e, SnapNotFound("foo", store.ErrSnapNotFound)}, 7722 {saXe, SnapNotFound("foo", store.ErrSnapNotFound)}, 7723 // action errors, unwrapped: 7724 {sa2e, BadRequest(`ERR: cannot refresh: snap not found: "bar", "foo"`)}, 7725 {saOe, BadRequest("ERR: cannot refresh, install, or download: other error")}, 7726 } 7727 7728 for _, t := range tests { 7729 com := check.Commentf("%v", t.err) 7730 rsp := errToResponse(t.err, []string{"foo"}, BadRequest, "%s: %v", "ERR") 7731 c.Check(rsp, check.DeepEquals, t.expectedRsp, com) 7732 } 7733 }