diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2016-07-14 07:57:32 +0200 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2016-07-14 07:57:32 +0200 |
| commit | 7acb1ad141993bcb536f9cdbe48c1a20997076aa (patch) | |
| tree | aaf10ad412fa8770bb6f6d0ee5b1efb05eaf562d | |
| parent | 93335ff94e2ef76018c28f0c254ce743bacfdc2d (diff) | |
| parent | 3ebb760b1f8b3ce16c627d7aa84c04acd41b1c74 (diff) | |
Merge remote-tracking branch 'upstream/master' into feature/rollback2
| -rw-r--r-- | client/buy.go | 51 | ||||
| -rw-r--r-- | cmd/snap/cmd_buy.go | 134 | ||||
| -rw-r--r-- | cmd/snap/cmd_buy_test.go | 232 | ||||
| -rw-r--r-- | cmd/snap/cmd_find.go | 31 | ||||
| -rw-r--r-- | overlord/auth/auth.go | 43 | ||||
| -rw-r--r-- | overlord/auth/auth_test.go | 72 | ||||
| -rw-r--r-- | overlord/managers_test.go | 2 | ||||
| -rw-r--r-- | overlord/snapstate/snapmgr.go | 3 | ||||
| -rw-r--r-- | snappy/install.go | 2 | ||||
| -rw-r--r-- | snappy/snapp_test.go | 4 | ||||
| -rw-r--r-- | store/auth.go | 38 | ||||
| -rw-r--r-- | store/auth_test.go | 49 | ||||
| -rw-r--r-- | store/store.go | 5 | ||||
| -rw-r--r-- | store/store_test.go | 68 | ||||
| -rwxr-xr-x | tests/lib/snaps/system-observe-consumer/bin/consumer | 11 | ||||
| -rw-r--r-- | tests/lib/snaps/system-observe-consumer/meta/snap.yaml | 9 | ||||
| -rw-r--r-- | tests/main/interfaces-system-observe/task.yaml | 46 |
17 files changed, 741 insertions, 59 deletions
diff --git a/client/buy.go b/client/buy.go new file mode 100644 index 0000000000..6fa400ecc4 --- /dev/null +++ b/client/buy.go @@ -0,0 +1,51 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package client + +import ( + "bytes" + "encoding/json" + + "github.com/snapcore/snapd/store" +) + +type BuyResult struct { + State string `json:"state"` +} + +func (client *Client) Buy(opts *store.BuyOptions) (*BuyResult, error) { + if opts == nil { + opts = &store.BuyOptions{} + } + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(opts); err != nil { + return nil, err + } + + var result BuyResult + _, err := client.doSync("POST", "/v2/buy", nil, nil, &body, &result) + + if err != nil { + return nil, err + } + + return &result, nil +} diff --git a/cmd/snap/cmd_buy.go b/cmd/snap/cmd_buy.go new file mode 100644 index 0000000000..85e41469a9 --- /dev/null +++ b/cmd/snap/cmd_buy.go @@ -0,0 +1,134 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main + +import ( + "bufio" + "fmt" + "strings" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/store" + + "github.com/jessevdk/go-flags" +) + +var shortBuyHelp = i18n.G("Buys a snap") +var longBuyHelp = i18n.G(` +The buy command buys a snap from the store. +`) + +var positiveResponse = map[string]bool{ + "": true, + i18n.G("y"): true, + i18n.G("yes"): true, +} + +type cmdBuy struct { + Currency string `long:"currency" description:"ISO 4217 code for currency (https://en.wikipedia.org/wiki/ISO_4217)"` + + Positional struct { + SnapName string `positional-arg-name:"<snap-name>"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("buy", shortBuyHelp, longBuyHelp, func() flags.Commander { + return &cmdBuy{} + }) +} + +func (x *cmdBuy) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return buySnap(&store.BuyOptions{ + SnapName: x.Positional.SnapName, + Currency: x.Currency, + }) +} + +func buySnap(opts *store.BuyOptions) error { + cli := Client() + + if strings.ContainsAny(opts.SnapName, ":*") { + return fmt.Errorf(i18n.G("cannot buy snap %q: invalid characters in name"), opts.SnapName) + } + + snaps, resultInfo, err := cli.Find(&client.FindOptions{ + Query: fmt.Sprintf("name:%s", opts.SnapName), + }) + + if err != nil { + return err + } + + if len(snaps) < 1 { + return fmt.Errorf(i18n.G("cannot find snap %q"), opts.SnapName) + } + + if len(snaps) > 1 { + return fmt.Errorf(i18n.G("cannot buy snap %q: muliple results found"), opts.SnapName) + } + + snap := snaps[0] + + opts.SnapID = snap.ID + opts.Channel = snap.Channel + if opts.Currency == "" { + opts.Currency = resultInfo.SuggestedCurrency + } + + opts.Price, opts.Currency, err = getPrice(snap.Prices, opts.Currency) + if err != nil { + return fmt.Errorf(i18n.G("cannot buy snap %q: %v"), opts.SnapName, err) + } + + if snap.Status == "available" { + return fmt.Errorf(i18n.G("cannot buy snap %q: it has already been bought"), opts.SnapName) + } + + reader := bufio.NewReader(nil) + reader.Reset(Stdin) + + fmt.Fprintf(Stdout, i18n.G("Do you want to buy %q from %q for %s? (Y/n): "), snap.Name, + snap.Developer, formatPrice(opts.Price, opts.Currency)) + + response, _, err := reader.ReadLine() + if err != nil { + return err + } + + if !positiveResponse[strings.ToLower(string(response))] { + return fmt.Errorf(i18n.G("aborting")) + } + + // TODO Handle pay backends that require user interaction + _, err = cli.Buy(opts) + if err != nil { + return err + } + + fmt.Fprintf(Stdout, "%s bought\n", opts.SnapName) + + return nil +} diff --git a/cmd/snap/cmd_buy_test.go b/cmd/snap/cmd_buy_test.go new file mode 100644 index 0000000000..98275f66a7 --- /dev/null +++ b/cmd/snap/cmd_buy_test.go @@ -0,0 +1,232 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +build !integrationcoverage + +/* + * Copyright (C) 2016 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main_test + +import ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestBuyHelp(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"buy"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "the required argument `<snap-name>` was not provided") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestBuyInvalidCharacters(c *check.C) { + _, err := snap.Parser().ParseArgs([]string{"buy", "a:b"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap \"a:b\": invalid characters in name") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") + + _, err = snap.Parser().ParseArgs([]string{"buy", "c*d"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap \"c*d\": invalid characters in name") + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") +} + +const buyFreeSnapFailsFindJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "available", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10" + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestBuyFreeSnapFails(c *check.C) { + getCount := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q.Get("q"), check.Equals, "name:hello") + fmt.Fprintln(w, buyFreeSnapFailsFindJson) + getCount++ + default: + c.Fatalf("unexpected HTTP method %q", r.Method) + } + }) + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "cannot buy snap \"hello\": snap is free") + c.Assert(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "") + c.Check(s.Stderr(), check.Equals, "") + c.Check(getCount, check.Equals, 1) +} + +const buySnapFindJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": [ + { + "channel": "stable", + "confinement": "strict", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "developer": "canonical", + "download-size": 65536, + "icon": "", + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "name": "hello", + "private": false, + "resource": "/v2/snaps/hello", + "revision": "1", + "status": "priced", + "summary": "GNU Hello, the \"hello world\" snap", + "type": "app", + "version": "2.10", + "prices": {"USD": 3.99, "GBP": 2.99} + } + ], + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +const buySnapJson = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "open_id": "https://login.staging.ubuntu.com/+id/open_id", + "snap_id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "refundable_until": "2015-07-15 18:46:21", + "state": "Complete" + }, + "sources": [ + "store" + ], + "suggested-currency": "GBP" +} +` + +func (s *SnapSuite) TestBuySnap(c *check.C) { + getCount := 0 + postCount := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q.Get("q"), check.Equals, "name:hello") + fmt.Fprintln(w, buySnapFindJson) + getCount++ + case "POST": + c.Check(r.URL.Path, check.Equals, "/v2/buy") + + var postData struct { + SnapID string `json:"snap-id"` + SnapName string `json:"snap-name"` + Channel string `json:"channel"` + Price float64 `json:"price"` + Currency string `json:"currency"` + } + decoder := json.NewDecoder(r.Body) + err := decoder.Decode(&postData) + c.Assert(err, check.IsNil) + + c.Check(postData.SnapID, check.Equals, "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6") + c.Check(postData.SnapName, check.Equals, "hello") + c.Check(postData.Channel, check.Equals, "stable") + c.Check(postData.Price, check.Equals, 2.99) + c.Check(postData.Currency, check.Equals, "GBP") + + fmt.Fprintln(w, buySnapJson) + postCount++ + default: + c.Fatalf("unexpected HTTP method %q", r.Method) + } + }) + + fmt.Fprint(s.stdin, "y\n") + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Check(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "Do you want to buy \"hello\" from \"canonical\" for 2.99GBP? (Y/n): hello bought\n") + c.Check(s.Stderr(), check.Equals, "") + c.Check(getCount, check.Equals, 1) + c.Check(postCount, check.Equals, 1) +} + +func (s *SnapSuite) TestBuyCancel(c *check.C) { + getCount := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + c.Check(r.URL.Path, check.Equals, "/v2/find") + q := r.URL.Query() + c.Check(q.Get("q"), check.Equals, "name:hello") + fmt.Fprintln(w, buySnapFindJson) + getCount++ + default: + c.Fatalf("unexpected HTTP method %q", r.Method) + } + }) + + fmt.Fprint(s.stdin, "no\n") + + rest, err := snap.Parser().ParseArgs([]string{"buy", "hello"}) + c.Assert(err, check.NotNil) + c.Check(err.Error(), check.Equals, "aborting") + c.Check(rest, check.DeepEquals, []string{"hello"}) + c.Check(s.Stdout(), check.Equals, "Do you want to buy \"hello\" from \"canonical\" for 2.99GBP? (Y/n): ") + c.Check(s.Stderr(), check.Equals, "") + c.Check(getCount, check.Equals, 1) +} diff --git a/cmd/snap/cmd_find.go b/cmd/snap/cmd_find.go index f489a3ad3b..ecc305fb6a 100644 --- a/cmd/snap/cmd_find.go +++ b/cmd/snap/cmd_find.go @@ -34,15 +34,10 @@ var longFindHelp = i18n.G(` The find command queries the store for available packages. `) -func getPrice(prices map[string]float64, currency, status string) string { +func getPrice(prices map[string]float64, currency string) (float64, string, error) { // If there are no prices, then the snap is free if len(prices) == 0 { - return "" - } - - // If the snap is priced, but has been purchased - if status == "available" { - return i18n.G("bought") + return 0, "", fmt.Errorf(i18n.G("snap is free")) } // Look up the price by currency code @@ -65,9 +60,29 @@ func getPrice(prices map[string]float64, currency, status string) string { } } + return val, currency, nil +} + +func formatPrice(val float64, currency string) string { return fmt.Sprintf("%.2f%s", val, currency) } +func getPriceString(prices map[string]float64, suggestedCurrency, status string) string { + price, currency, err := getPrice(prices, suggestedCurrency) + + // If there are no prices, then the snap is free + if err != nil { + return "" + } + + // If the snap is priced, but has been purchased + if status == "available" { + return i18n.G("bought") + } + + return formatPrice(price, currency) +} + type cmdFind struct { Positional struct { Query string `positional-arg-name:"<query>"` @@ -112,7 +127,7 @@ func findSnaps(opts *client.FindOptions) error { notes := &Notes{ Private: snap.Private, Confinement: snap.Confinement, - Price: getPrice(snap.Prices, resInfo.SuggestedCurrency, snap.Status), + Price: getPriceString(snap.Prices, resInfo.SuggestedCurrency, snap.Status), } // TODO: get snap.Publisher, so we can only show snap.Developer if it's different fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", snap.Name, snap.Version, snap.Developer, notes, snap.Summary) diff --git a/overlord/auth/auth.go b/overlord/auth/auth.go index 9e9725b8cd..c71364bc08 100644 --- a/overlord/auth/auth.go +++ b/overlord/auth/auth.go @@ -119,6 +119,26 @@ func User(st *state.State, id int) (*UserState, error) { return nil, fmt.Errorf("invalid user") } +// UpdateUser updates user in state +func UpdateUser(st *state.State, user *UserState) error { + var authStateData AuthState + + err := st.Get("auth", &authStateData) + if err != nil { + return err + } + + for i := range authStateData.Users { + if authStateData.Users[i].ID == user.ID { + authStateData.Users[i] = *user + st.Set("auth", authStateData) + return nil + } + } + + return fmt.Errorf("invalid user") +} + // Device returns the device details from the state. func Device(st *state.State) (*DeviceState, error) { var authStateData AuthState @@ -183,3 +203,26 @@ NextUser: } return nil, ErrInvalidAuth } + +// An AuthContext handles user updates. +type AuthContext interface { + UpdateUser(user *UserState) error +} + +// authContext helps keeping track and updating users in the state. +type authContext struct { + state *state.State +} + +// NewAuthContext returns an AuthContext for state. +func NewAuthContext(st *state.State) AuthContext { + return &authContext{state: st} +} + +// UpdateUser updates user in state. +func (ac *authContext) UpdateUser(user *UserState) error { + ac.state.Lock() + defer ac.state.Unlock() + + return UpdateUser(ac.state, user) +} diff --git a/overlord/auth/auth_test.go b/overlord/auth/auth_test.go index 5e3ffac0b5..657b847973 100644 --- a/overlord/auth/auth_test.go +++ b/overlord/auth/auth_test.go @@ -199,6 +199,43 @@ func (as *authSuite) TestUser(c *C) { c.Check(userFromState, DeepEquals, user) } +func (as *authSuite) TestUpdateUser(c *C) { + as.state.Lock() + user, _ := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) + as.state.Unlock() + + user.Username = "different" + user.StoreDischarges = []string{"updated-discharge"} + + as.state.Lock() + err := auth.UpdateUser(as.state, user) + as.state.Unlock() + c.Check(err, IsNil) + + as.state.Lock() + userFromState, err := auth.User(as.state, user.ID) + as.state.Unlock() + c.Check(err, IsNil) + c.Check(userFromState, DeepEquals, user) +} + +func (as *authSuite) TestUpdateUserInvalid(c *C) { + as.state.Lock() + _, _ = auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) + as.state.Unlock() + + user := &auth.UserState{ + ID: 102, + Username: "username", + Macaroon: "macaroon", + } + + as.state.Lock() + err := auth.UpdateUser(as.state, user) + as.state.Unlock() + c.Assert(err, ErrorMatches, "invalid user") +} + func (as *authSuite) TestRemove(c *C) { as.state.Lock() user, err := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) @@ -241,3 +278,38 @@ func (as *authSuite) TestSetDevice(c *C) { c.Check(err, IsNil) c.Check(device, DeepEquals, &auth.DeviceState{Brand: "some-brand"}) } + +func (as *authSuite) TestAuthContextUpdateUser(c *C) { + as.state.Lock() + user, _ := auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) + as.state.Unlock() + + user.Username = "different" + user.StoreDischarges = []string{"updated-discharge"} + + authContext := auth.NewAuthContext(as.state) + err := authContext.UpdateUser(user) + c.Check(err, IsNil) + + as.state.Lock() + userFromState, err := auth.User(as.state, user.ID) + as.state.Unlock() + c.Check(err, IsNil) + c.Check(userFromState, DeepEquals, user) +} + +func (as *authSuite) TestAuthContextUpdateUserInvalid(c *C) { + as.state.Lock() + _, _ = auth.NewUser(as.state, "username", "macaroon", []string{"discharge"}) + as.state.Unlock() + + user := &auth.UserState{ + ID: 102, + Username: "username", + Macaroon: "macaroon", + } + + authContext := auth.NewAuthContext(as.state) + err := authContext.UpdateUser(user) + c.Assert(err, ErrorMatches, "invalid user") +} diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 9771202ef2..0f5acc1a3f 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -267,7 +267,7 @@ apps: BulkURI: bulkURL, } - mStore := store.NewUbuntuStoreSnapRepository(&storeCfg, "") + mStore := store.NewUbuntuStoreSnapRepository(&storeCfg, "", nil) st := ms.o.State() st.Lock() diff --git a/overlord/snapstate/snapmgr.go b/overlord/snapstate/snapmgr.go index ddca3cfa29..c9090c4c41 100644 --- a/overlord/snapstate/snapmgr.go +++ b/overlord/snapstate/snapmgr.go @@ -403,7 +403,8 @@ func Store(s *state.State) StoreService { storeID = cand } - s.Cache(cachedStoreKey{}, store.NewUbuntuStoreSnapRepository(nil, storeID)) + authContext := auth.NewAuthContext(s) + s.Cache(cachedStoreKey{}, store.NewUbuntuStoreSnapRepository(nil, storeID, authContext)) return cachedStore(s) } diff --git a/snappy/install.go b/snappy/install.go index c7ca1aeeab..c83333b333 100644 --- a/snappy/install.go +++ b/snappy/install.go @@ -75,7 +75,7 @@ func newConfiguredUbuntuStoreSnapRepository() *store.SnapUbuntuStoreRepository { storeID = cand } - return store.NewUbuntuStoreSnapRepository(storeConfig, storeID) + return store.NewUbuntuStoreSnapRepository(storeConfig, storeID, nil) } // Install the givens snap names provided via args. This can be local diff --git a/snappy/snapp_test.go b/snappy/snapp_test.go index cd49fde689..e3bedeca3c 100644 --- a/snappy/snapp_test.go +++ b/snappy/snapp_test.go @@ -176,7 +176,7 @@ func (s *SnapTestSuite) TestUbuntuStoreRepositoryInstallRemoteSnap(c *C) { r.DownloadURL = mockServer.URL + "/snap" r.IconURL = mockServer.URL + "/icon" - mStore := store.NewUbuntuStoreSnapRepository(s.storeCfg, "") + mStore := store.NewUbuntuStoreSnapRepository(s.storeCfg, "", nil) p := &MockProgressMeter{} name, err := installRemote(mStore, r, LegacyInhibitHooks, p) c.Assert(err, IsNil) @@ -233,7 +233,7 @@ apps: r.DownloadURL = mockServer.URL + "/snap" r.IconURL = mockServer.URL + "/icon" - mStore := store.NewUbuntuStoreSnapRepository(s.storeCfg, "") + mStore := store.NewUbuntuStoreSnapRepository(s.storeCfg, "", nil) p := &MockProgressMeter{} name, err := installRemote(mStore, r, LegacyInhibitHooks, p) c.Assert(err, IsNil) diff --git a/store/auth.go b/store/auth.go index 814a7a328a..3158026821 100644 --- a/store/auth.go +++ b/store/auth.go @@ -38,6 +38,8 @@ var ( UbuntuoneLocation = authLocation() // UbuntuoneDischargeAPI points to SSO endpoint to discharge a macaroon UbuntuoneDischargeAPI = ubuntuoneAPIBase + "/tokens/discharge" + // UbuntuoneRefreshDischargeAPI points to SSO endpoint to refresh a discharge macaroon + UbuntuoneRefreshDischargeAPI = ubuntuoneAPIBase + "/tokens/refresh" ) type ssoMsg struct { @@ -137,24 +139,15 @@ func RequestStoreMacaroon() (string, error) { return responseData.Macaroon, nil } -// DischargeAuthCaveat returns a macaroon with the store auth caveat discharged. -func DischargeAuthCaveat(caveat, username, password, otp string) (string, error) { +func requestDischargeMacaroon(endpoint string, data map[string]string) (string, error) { const errorPrefix = "cannot authenticate on snap store: " - data := map[string]string{ - "email": username, - "password": password, - "caveat_id": caveat, - } - if otp != "" { - data["otp"] = otp - } dischargeJSONData, err := json.Marshal(data) if err != nil { return "", fmt.Errorf(errorPrefix+"%v", err) } - req, err := http.NewRequest("POST", UbuntuoneDischargeAPI, strings.NewReader(string(dischargeJSONData))) + req, err := http.NewRequest("POST", endpoint, strings.NewReader(string(dischargeJSONData))) if err != nil { return "", fmt.Errorf(errorPrefix+"%v", err) } @@ -207,3 +200,26 @@ func DischargeAuthCaveat(caveat, username, password, otp string) (string, error) } return responseData.Macaroon, nil } + +// DischargeAuthCaveat returns a macaroon with the store auth caveat discharged. +func DischargeAuthCaveat(caveat, username, password, otp string) (string, error) { + data := map[string]string{ + "email": username, + "password": password, + "caveat_id": caveat, + } + if otp != "" { + data["otp"] = otp + } + + return requestDischargeMacaroon(UbuntuoneDischargeAPI, data) +} + +// RefreshDischargeMacaroon returns a soft-refreshed discharge macaroon. +func RefreshDischargeMacaroon(discharge string) (string, error) { + data := map[string]string{ + "discharge_macaroon": discharge, + } + + return requestDischargeMacaroon(UbuntuoneRefreshDischargeAPI, data) +} diff --git a/store/auth_test.go b/store/auth_test.go index 0992d2360f..644cf7dc2a 100644 --- a/store/auth_test.go +++ b/store/auth_test.go @@ -184,6 +184,55 @@ func (s *authTestSuite) TestDischargeAuthCaveatError(c *C) { c.Assert(discharge, Equals, "") } +func (s *authTestSuite) TestRefreshDischargeMacaroon(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, mockStoreReturnDischarge) + })) + defer mockServer.Close() + UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" + + discharge, err := RefreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon") + c.Assert(err, IsNil) + c.Assert(discharge, Equals, "the-discharge-macaroon-serialized-data") +} + +func (s *authTestSuite) TestRefreshDischargeMacaroonInvalidLogin(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(mockStoreInvalidLoginCode) + io.WriteString(w, mockStoreInvalidLogin) + })) + defer mockServer.Close() + UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" + + discharge, err := RefreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon") + c.Assert(err, ErrorMatches, "cannot authenticate on snap store: Provided email/password is not correct.") + c.Assert(discharge, Equals, "") +} + +func (s *authTestSuite) TestRefreshDischargeMacaroonMissingData(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, mockStoreReturnNoMacaroon) + })) + defer mockServer.Close() + UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" + + discharge, err := RefreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon") + c.Assert(err, ErrorMatches, "cannot authenticate on snap store: empty macaroon returned") + c.Assert(discharge, Equals, "") +} + +func (s *authTestSuite) TestRefreshDischargeMacaroonError(c *C) { + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + defer mockServer.Close() + UbuntuoneRefreshDischargeAPI = mockServer.URL + "/tokens/refresh" + + discharge, err := RefreshDischargeMacaroon("soft-expired-serialized-discharge-macaroon") + c.Assert(err, ErrorMatches, "cannot authenticate on snap store: server returned status 500") + c.Assert(discharge, Equals, "") +} + func (s *authTestSuite) TestMacaroonSerialize(c *C) { m, err := makeTestMacaroon() c.Check(err, IsNil) diff --git a/store/store.go b/store/store.go index 6fcf9cbee9..d90b1c84f0 100644 --- a/store/store.go +++ b/store/store.go @@ -119,6 +119,8 @@ type SnapUbuntuStoreRepository struct { // reused http client client *http.Client + authContext auth.AuthContext + mu sync.Mutex suggestedCurrency string } @@ -236,7 +238,7 @@ type searchResults struct { var detailFields = getStructFields(snapDetails{}) // NewUbuntuStoreSnapRepository creates a new SnapUbuntuStoreRepository with the given access configuration and for given the store id. -func NewUbuntuStoreSnapRepository(cfg *SnapUbuntuStoreConfig, storeID string) *SnapUbuntuStoreRepository { +func NewUbuntuStoreSnapRepository(cfg *SnapUbuntuStoreConfig, storeID string, authContext auth.AuthContext) *SnapUbuntuStoreRepository { if cfg == nil { cfg = &defaultConfig } @@ -282,6 +284,7 @@ func NewUbuntuStoreSnapRepository(cfg *SnapUbuntuStoreConfig, storeID string) *S Key: "SNAPD_DEBUG_HTTP", }, }, + authContext: authContext, } } diff --git a/store/store_test.go b/store/store_test.go index 7ced920d27..f553a82367 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -99,7 +99,7 @@ func createTestUser(userID int, root, discharge *macaroon.Macaroon) (*auth.UserS } func (t *remoteRepoTestSuite) SetUpTest(c *C) { - t.store = NewUbuntuStoreSnapRepository(nil, "") + t.store = NewUbuntuStoreSnapRepository(nil, "", nil) t.origDownloadFunc = download dirs.SetRootDir(c.MkDir()) c.Assert(os.MkdirAll(dirs.SnapSnapsDir, 0755), IsNil) @@ -398,7 +398,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) { cfg := SnapUbuntuStoreConfig{ DetailsURI: detailsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // the actual test @@ -457,7 +457,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsDevmode(c *C) { cfg := SnapUbuntuStoreConfig{ DetailsURI: detailsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // the actual test @@ -505,7 +505,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsSetsAuth(c *C) { DetailsURI: detailsURI, PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) snap, err := repo.Snap("hello-world", "edge", false, t.user) @@ -533,7 +533,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsOopses(c *C) { cfg := SnapUbuntuStoreConfig{ DetailsURI: detailsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // the actual test @@ -577,7 +577,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNoDetails(c *C) { cfg := SnapUbuntuStoreConfig{ DetailsURI: detailsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // the actual test @@ -690,7 +690,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindQueries(c *C) { DetailsURI: detailsURI, SearchURI: searchURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) for _, query := range []string{ @@ -704,7 +704,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindQueries(c *C) { } func (t *remoteRepoTestSuite) TestUbuntuStoreFindFailures(c *C) { - repo := NewUbuntuStoreSnapRepository(&SnapUbuntuStoreConfig{SearchURI: new(url.URL)}, "") + repo := NewUbuntuStoreSnapRepository(&SnapUbuntuStoreConfig{SearchURI: new(url.URL)}, "", nil) _, err := repo.Find("", "", nil) c.Check(err, Equals, ErrEmptyQuery) _, err = repo.Find("foo:bar", "", nil) @@ -740,7 +740,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindFails(c *C) { SearchURI: searchURI, DetailFields: []string{}, // make the error less noisy } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) snaps, err := repo.Find("hello", "", nil) @@ -763,7 +763,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadContentType(c *C) { SearchURI: searchURI, DetailFields: []string{}, // make the error less noisy } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) snaps, err := repo.Find("hello", "", nil) @@ -789,7 +789,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadBody(c *C) { SearchURI: searchURI, DetailFields: []string{}, // make the error less noisy } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) snaps, err := repo.Find("hello", "", nil) @@ -831,7 +831,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindSetsAuth(c *C) { SearchURI: searchURI, PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) snaps, err := repo.Find("foo", "", t.user) @@ -876,7 +876,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreFindAuthFailed(c *C) { PurchasesURI: purchasesURI, DetailFields: []string{}, // make the error less noisy } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) snaps, err := repo.Find("foo", "", t.user) @@ -980,7 +980,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefresh(c *C) { cfg := SnapUbuntuStoreConfig{ BulkURI: bulkURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) results, err := repo.ListRefresh([]*RefreshCandidate{ @@ -1033,7 +1033,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipCurrent(c cfg := SnapUbuntuStoreConfig{ BulkURI: bulkURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) results, err := repo.ListRefresh([]*RefreshCandidate{ @@ -1082,7 +1082,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipBlocked(c cfg := SnapUbuntuStoreConfig{ BulkURI: bulkURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) results, err := repo.ListRefresh([]*RefreshCandidate{ @@ -1130,7 +1130,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdateNotSendLocalRevs(c cfg := SnapUbuntuStoreConfig{ BulkURI: bulkURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) _, err = repo.ListRefresh([]*RefreshCandidate{ @@ -1163,7 +1163,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdatesSetsAuth(c *C) { cfg := SnapUbuntuStoreConfig{ BulkURI: bulkURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) _, err = repo.ListRefresh([]*RefreshCandidate{ @@ -1279,7 +1279,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion(c *C) { cfg := SnapUbuntuStoreConfig{ AssertionsURI: assertionsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) a, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil) c.Assert(err, IsNil) @@ -1307,7 +1307,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionSetsAuth(c *C) { cfg := SnapUbuntuStoreConfig{ AssertionsURI: assertionsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) _, err = repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, t.user) c.Assert(err, IsNil) @@ -1331,7 +1331,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNotFound(c *C) { cfg := SnapUbuntuStoreConfig{ AssertionsURI: assertionsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) _, err = repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil) c.Check(err, Equals, ErrAssertionNotFound) @@ -1355,7 +1355,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) { cfg := SnapUbuntuStoreConfig{ DetailsURI: detailsURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // the store doesn't know the currency until after the first search, so fall back to dollars @@ -1393,7 +1393,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchases(c *C) { cfg := SnapUbuntuStoreConfig{ PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1440,7 +1440,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchasesFailedAccess(c *C) cfg := SnapUbuntuStoreConfig{ PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1471,7 +1471,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchasesFailedAccess(c *C) func (t *remoteRepoTestSuite) TestUbuntuStoreDecoratePurchasesNoAuth(c *C) { cfg := SnapUbuntuStoreConfig{} - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1517,7 +1517,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreGetPurchasesAllFree(c *C) { PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // This snap is free @@ -1554,7 +1554,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreGetPurchasesSingle(c *C) { cfg := SnapUbuntuStoreConfig{ PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1570,7 +1570,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreGetPurchasesSingle(c *C) { func (t *remoteRepoTestSuite) TestUbuntuStoreGetPurchasesSingleFreeSnap(c *C) { cfg := SnapUbuntuStoreConfig{} - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1602,7 +1602,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreGetPurchasesSingleNotFound(c *C) { cfg := SnapUbuntuStoreConfig{ PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1635,7 +1635,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreGetPurchasesTokenExpired(c *C) { cfg := SnapUbuntuStoreConfig{ PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) helloWorld := &snap.Info{} @@ -1724,7 +1724,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreBuySuccess(c *C) { DetailsURI: detailsURI, PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // Find the snap first @@ -1806,7 +1806,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailWrongPrice(c *C) { DetailsURI: detailsURI, PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // Find the snap first @@ -1885,7 +1885,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailNotFound(c *C) { DetailsURI: detailsURI, PurchasesURI: purchasesURI, } - repo := NewUbuntuStoreSnapRepository(&cfg, "") + repo := NewUbuntuStoreSnapRepository(&cfg, "", nil) c.Assert(repo, NotNil) // Find the snap first @@ -1911,7 +1911,7 @@ func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailNotFound(c *C) { } func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailArgumentChecking(c *C) { - repo := NewUbuntuStoreSnapRepository(&SnapUbuntuStoreConfig{}, "") + repo := NewUbuntuStoreSnapRepository(&SnapUbuntuStoreConfig{}, "", nil) c.Assert(repo, NotNil) // no snap ID diff --git a/tests/lib/snaps/system-observe-consumer/bin/consumer b/tests/lib/snaps/system-observe-consumer/bin/consumer new file mode 100755 index 0000000000..2a7e7babef --- /dev/null +++ b/tests/lib/snaps/system-observe-consumer/bin/consumer @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 + +import os +import sys + +def run(): + with open('/proc/tty/drivers', 'r') as f: + print(f.read()) + +if __name__ == '__main__': + sys.exit(run()) diff --git a/tests/lib/snaps/system-observe-consumer/meta/snap.yaml b/tests/lib/snaps/system-observe-consumer/meta/snap.yaml new file mode 100644 index 0000000000..3ea1a35d3d --- /dev/null +++ b/tests/lib/snaps/system-observe-consumer/meta/snap.yaml @@ -0,0 +1,9 @@ +name: system-observe-consumer +version: 1.0 +summary: Basic system-observe consumer snap +description: A basic snap declaring a plug on system-observe + +apps: + system-observe-consumer: + command: bin/consumer + plugs: [system-observe] diff --git a/tests/main/interfaces-system-observe/task.yaml b/tests/main/interfaces-system-observe/task.yaml new file mode 100644 index 0000000000..33fa54383b --- /dev/null +++ b/tests/main/interfaces-system-observe/task.yaml @@ -0,0 +1,46 @@ +summary: Ensures that the system-observe interface works. + +details: | + A snap declaring the system-observe plug is defined, its command + just calls ps -ax. + + The test itself checks for the lack of autoconnect and then tries + to execute the snap command with the plug connected (it must succeed) + and disconnected (it must fail). + +prepare: | + echo "Given a snap declaring a plug on the system-observe interface is installed" + snapbuild $TESTSLIB/snaps/system-observe-consumer . + snap install system-observe-consumer_1.0_all.snap + +restore: | + rm -f system-observe-consumer_1.0_all.snap + +execute: | + CONNECTED_PATTERN=":system-observe +system-observe-consumer" + DISCONNECTED_PATTERN="(?s).*?\n- +system-observe-consumer:system-observe" + + echo "Then the plug is shown as disconnected" + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "===========================================" + + echo "When the plug is connected" + snap connect system-observe-consumer:system-observe ubuntu-core:system-observe + snap interfaces | grep -Pzq "$CONNECTED_PATTERN" + + echo "Then the snap is able to get system information" + expected="(?s)/dev/tty.*?serial" + sudo -i -u test /bin/sh -c "system-observe-consumer" | grep -Pq "$expected" + + echo "===========================================" + + echo "When the plug is disconnected" + snap disconnect system-observe-consumer:system-observe ubuntu-core:system-observe + snap interfaces | grep -Pzq "$DISCONNECTED_PATTERN" + + echo "Then the snap is not able to get system information" + if sudo -i -u test /bin/sh -c "system-observe-consumer"; then + echo "Expected error with plug disconnected" + exit 1 + fi |
