summaryrefslogtreecommitdiff
diff options
authorMichael Vogt <mvo@ubuntu.com>2016-07-14 07:57:32 +0200
committerMichael Vogt <mvo@ubuntu.com>2016-07-14 07:57:32 +0200
commit7acb1ad141993bcb536f9cdbe48c1a20997076aa (patch)
treeaaf10ad412fa8770bb6f6d0ee5b1efb05eaf562d
parent93335ff94e2ef76018c28f0c254ce743bacfdc2d (diff)
parent3ebb760b1f8b3ce16c627d7aa84c04acd41b1c74 (diff)
Merge remote-tracking branch 'upstream/master' into feature/rollback2
-rw-r--r--client/buy.go51
-rw-r--r--cmd/snap/cmd_buy.go134
-rw-r--r--cmd/snap/cmd_buy_test.go232
-rw-r--r--cmd/snap/cmd_find.go31
-rw-r--r--overlord/auth/auth.go43
-rw-r--r--overlord/auth/auth_test.go72
-rw-r--r--overlord/managers_test.go2
-rw-r--r--overlord/snapstate/snapmgr.go3
-rw-r--r--snappy/install.go2
-rw-r--r--snappy/snapp_test.go4
-rw-r--r--store/auth.go38
-rw-r--r--store/auth_test.go49
-rw-r--r--store/store.go5
-rw-r--r--store/store_test.go68
-rwxr-xr-xtests/lib/snaps/system-observe-consumer/bin/consumer11
-rw-r--r--tests/lib/snaps/system-observe-consumer/meta/snap.yaml9
-rw-r--r--tests/main/interfaces-system-observe/task.yaml46
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