Permalink
Fetching contributors…
Cannot retrieve contributors at this time
5212 lines (4524 sloc) 161 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2017 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 store
import (
"bytes"
"crypto"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
"golang.org/x/crypto/sha3"
"golang.org/x/net/context"
. "gopkg.in/check.v1"
"gopkg.in/macaroon.v1"
"gopkg.in/retry.v1"
"github.com/snapcore/snapd/advisor"
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/httputil"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/progress"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
)
func TestStore(t *testing.T) { TestingT(t) }
type configTestSuite struct{}
var _ = Suite(&configTestSuite{})
func (suite *configTestSuite) TestSetBaseURL(c *C) {
// Sanity check to prove at least one URI changes.
cfg := DefaultConfig()
c.Assert(cfg.StoreBaseURL.String(), Equals, "https://api.snapcraft.io/")
u, err := url.Parse("http://example.com/path/prefix/")
c.Assert(err, IsNil)
err = cfg.setBaseURL(u)
c.Assert(err, IsNil)
c.Check(cfg.StoreBaseURL.String(), Equals, "http://example.com/path/prefix/")
c.Check(cfg.AssertionsBaseURL, IsNil)
}
func (suite *configTestSuite) TestSetBaseURLStoreOverrides(c *C) {
cfg := DefaultConfig()
c.Assert(cfg.setBaseURL(apiURL()), IsNil)
c.Check(cfg.StoreBaseURL, Matches, apiURL().String()+".*")
c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "https://force-api.local/"), IsNil)
defer os.Setenv("SNAPPY_FORCE_API_URL", "")
cfg = DefaultConfig()
c.Assert(cfg.setBaseURL(apiURL()), IsNil)
c.Check(cfg.StoreBaseURL.String(), Equals, "https://force-api.local/")
c.Check(cfg.AssertionsBaseURL, IsNil)
}
func (suite *configTestSuite) TestSetBaseURLStoreURLBadEnviron(c *C) {
c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "://example.com"), IsNil)
defer os.Setenv("SNAPPY_FORCE_API_URL", "")
cfg := DefaultConfig()
err := cfg.setBaseURL(apiURL())
c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_API_URL: parse ://example.com: missing protocol scheme")
}
func (suite *configTestSuite) TestSetBaseURLAssertsOverrides(c *C) {
cfg := DefaultConfig()
c.Assert(cfg.setBaseURL(apiURL()), IsNil)
c.Check(cfg.AssertionsBaseURL, IsNil)
c.Assert(os.Setenv("SNAPPY_FORCE_SAS_URL", "https://force-sas.local/"), IsNil)
defer os.Setenv("SNAPPY_FORCE_SAS_URL", "")
cfg = DefaultConfig()
c.Assert(cfg.setBaseURL(apiURL()), IsNil)
c.Check(cfg.AssertionsBaseURL, Matches, "https://force-sas.local/.*")
}
func (suite *configTestSuite) TestSetBaseURLAssertsURLBadEnviron(c *C) {
c.Assert(os.Setenv("SNAPPY_FORCE_SAS_URL", "://example.com"), IsNil)
defer os.Setenv("SNAPPY_FORCE_SAS_URL", "")
cfg := DefaultConfig()
err := cfg.setBaseURL(apiURL())
c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_SAS_URL: parse ://example.com: missing protocol scheme")
}
const (
// Store API paths/patterns.
authNoncesPath = "/api/v1/snaps/auth/nonces"
authSessionPath = "/api/v1/snaps/auth/sessions"
buyPath = "/api/v1/snaps/purchases/buy"
customersMePath = "/api/v1/snaps/purchases/customers/me"
detailsPathPattern = "/api/v1/snaps/details/.*"
metadataPath = "/api/v1/snaps/metadata"
ordersPath = "/api/v1/snaps/purchases/orders"
searchPath = "/api/v1/snaps/search"
sectionsPath = "/api/v1/snaps/sections"
)
// Build details path for a snap name.
func detailsPath(snapName string) string {
return strings.Replace(detailsPathPattern, ".*", snapName, 1)
}
// Assert that a request is roughly as expected. Useful in fakes that should
// only attempt to handle a specific request.
func assertRequest(c *C, r *http.Request, method, pathPattern string) {
pathMatch, err := regexp.MatchString("^"+pathPattern+"$", r.URL.Path)
c.Assert(err, IsNil)
if r.Method != method || !pathMatch {
c.Fatalf("request didn't match (expected %s %s, got %s %s)", method, pathPattern, r.Method, r.URL.Path)
}
}
type remoteRepoTestSuite struct {
testutil.BaseTest
store *Store
logbuf *bytes.Buffer
user *auth.UserState
localUser *auth.UserState
device *auth.DeviceState
origDownloadFunc func(context.Context, string, string, string, *auth.UserState, *Store, io.ReadWriteSeeker, int64, progress.Meter) error
mockXDelta *testutil.MockCmd
restoreLogger func()
}
var _ = Suite(&remoteRepoTestSuite{})
const (
exModel = `type: model
authority-id: my-brand
series: 16
brand-id: my-brand
model: baz-3000
architecture: armhf
gadget: gadget
kernel: kernel
store: my-brand-store-id
timestamp: 2016-08-20T13:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
AXNpZw=`
exSerial = `type: serial
authority-id: my-brand
brand-id: my-brand
model: baz-3000
serial: 9999
device-key:
AcbBTQRWhcGAARAAtJGIguK7FhSyRxL/6jvdy0zAgGCjC1xVNFzeF76p5G8BXNEEHZUHK+z8Gr2J
inVrpvhJhllf5Ob2dIMH2YQbC9jE1kjbzvuauQGDqk6tNQm0i3KDeHCSPgVN+PFXPwKIiLrh66Po
AC7OfR1rFUgCqu0jch0H6Nue0ynvEPiY4dPeXq7mCdpDr5QIAM41L+3hg0OdzvO8HMIGZQpdF6jP
7fkkVMROYvHUOJ8kknpKE7FiaNNpH7jK1qNxOYhLeiioX0LYrdmTvdTWHrSKZc82ZmlDjpKc4hUx
VtTXMAysw7CzIdREPom/vJklnKLvZt+Wk5AEF5V5YKnuT3pY+fjVMZ56GtTEeO/Er/oLk/n2xUK5
fD5DAyW/9z0ygzwTbY5IuWXyDfYneL4nXwWOEgg37Z4+8mTH+ftTz2dl1x1KIlIR2xo0kxf9t8K+
jlr13vwF1+QReMCSUycUsZ2Eep5XhjI+LG7G1bMSGqodZTIOXLkIy6+3iJ8Z/feIHlJ0ELBDyFbl
Yy04Sf9LI148vJMsYenonkoWejWdMi8iCUTeaZydHJEUBU/RbNFLjCWa6NIUe9bfZgLiOOZkps54
+/AL078ri/tGjo/5UGvezSmwrEoWJyqrJt2M69N2oVDLJcHeo2bUYPtFC2Kfb2je58JrJ+llifdg
rAsxbnHXiXyVimUAEQEAAQ==
device-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
timestamp: 2016-08-24T21:55:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
AXNpZw=`
exDeviceSessionRequest = `type: device-session-request
brand-id: my-brand
model: baz-3000
serial: 9999
nonce: @NONCE@
timestamp: 2016-08-24T21:55:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
AXNpZw=`
)
type testAuthContext struct {
c *C
device *auth.DeviceState
user *auth.UserState
proxyStoreID string
proxyStoreURL *url.URL
storeID string
}
func (ac *testAuthContext) Device() (*auth.DeviceState, error) {
freshDevice := auth.DeviceState{}
if ac.device != nil {
freshDevice = *ac.device
}
return &freshDevice, nil
}
func (ac *testAuthContext) UpdateDeviceAuth(d *auth.DeviceState, newSessionMacaroon string) (*auth.DeviceState, error) {
ac.c.Assert(d, DeepEquals, ac.device)
updated := *ac.device
updated.SessionMacaroon = newSessionMacaroon
*ac.device = updated
return &updated, nil
}
func (ac *testAuthContext) UpdateUserAuth(u *auth.UserState, newDischarges []string) (*auth.UserState, error) {
ac.c.Assert(u, DeepEquals, ac.user)
updated := *ac.user
updated.StoreDischarges = newDischarges
return &updated, nil
}
func (ac *testAuthContext) StoreID(fallback string) (string, error) {
if ac.storeID != "" {
return ac.storeID, nil
}
return fallback, nil
}
func (ac *testAuthContext) DeviceSessionRequestParams(nonce string) (*auth.DeviceSessionRequestParams, error) {
model, err := asserts.Decode([]byte(exModel))
if err != nil {
return nil, err
}
serial, err := asserts.Decode([]byte(exSerial))
if err != nil {
return nil, err
}
sessReq, err := asserts.Decode([]byte(strings.Replace(exDeviceSessionRequest, "@NONCE@", nonce, 1)))
if err != nil {
return nil, err
}
return &auth.DeviceSessionRequestParams{
Request: sessReq.(*asserts.DeviceSessionRequest),
Serial: serial.(*asserts.Serial),
Model: model.(*asserts.Model),
}, nil
}
func (ac *testAuthContext) ProxyStoreParams(defaultURL *url.URL) (string, *url.URL, error) {
if ac.proxyStoreID != "" {
return ac.proxyStoreID, ac.proxyStoreURL, nil
}
return "", defaultURL, nil
}
func makeTestMacaroon() (*macaroon.Macaroon, error) {
m, err := macaroon.New([]byte("secret"), "some-id", "location")
if err != nil {
return nil, err
}
err = m.AddThirdPartyCaveat([]byte("shared-key"), "third-party-caveat", UbuntuoneLocation)
if err != nil {
return nil, err
}
return m, nil
}
func makeTestDischarge() (*macaroon.Macaroon, error) {
m, err := macaroon.New([]byte("shared-key"), "third-party-caveat", UbuntuoneLocation)
if err != nil {
return nil, err
}
return m, nil
}
func makeTestRefreshDischargeResponse() (string, error) {
m, err := macaroon.New([]byte("shared-key"), "refreshed-third-party-caveat", UbuntuoneLocation)
if err != nil {
return "", err
}
return auth.MacaroonSerialize(m)
}
func createTestUser(userID int, root, discharge *macaroon.Macaroon) (*auth.UserState, error) {
serializedMacaroon, err := auth.MacaroonSerialize(root)
if err != nil {
return nil, err
}
serializedDischarge, err := auth.MacaroonSerialize(discharge)
if err != nil {
return nil, err
}
return &auth.UserState{
ID: userID,
Username: "test-user",
Macaroon: serializedMacaroon,
Discharges: []string{serializedDischarge},
StoreMacaroon: serializedMacaroon,
StoreDischarges: []string{serializedDischarge},
}, nil
}
func createTestDevice() *auth.DeviceState {
return &auth.DeviceState{
Brand: "some-brand",
SessionMacaroon: "device-macaroon",
Serial: "9999",
}
}
func (t *remoteRepoTestSuite) SetUpTest(c *C) {
t.store = New(nil, nil)
t.origDownloadFunc = download
dirs.SetRootDir(c.MkDir())
c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), IsNil)
os.Setenv("SNAPD_DEBUG", "1")
t.AddCleanup(func() { os.Unsetenv("SNAPD_DEBUG") })
t.logbuf, t.restoreLogger = logger.MockLogger()
root, err := makeTestMacaroon()
c.Assert(err, IsNil)
discharge, err := makeTestDischarge()
c.Assert(err, IsNil)
t.user, err = createTestUser(1, root, discharge)
c.Assert(err, IsNil)
t.localUser = &auth.UserState{
ID: 11,
Username: "test-user",
Macaroon: "snapd-macaroon",
}
t.device = createTestDevice()
t.mockXDelta = testutil.MockCommand(c, "xdelta3", "")
MockDefaultRetryStrategy(&t.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second,
retry.Exponential{
Initial: 1 * time.Millisecond,
Factor: 1,
},
)))
}
func (t *remoteRepoTestSuite) TearDownTest(c *C) {
download = t.origDownloadFunc
t.mockXDelta.Restore()
t.restoreLogger()
}
func (t *remoteRepoTestSuite) expectedAuthorization(c *C, user *auth.UserState) string {
var buf bytes.Buffer
root, err := auth.MacaroonDeserialize(user.StoreMacaroon)
c.Assert(err, IsNil)
discharge, err := auth.MacaroonDeserialize(user.StoreDischarges[0])
c.Assert(err, IsNil)
discharge.Bind(root.Signature())
serializedMacaroon, err := auth.MacaroonSerialize(root)
c.Assert(err, IsNil)
serializedDischarge, err := auth.MacaroonSerialize(discharge)
c.Assert(err, IsNil)
fmt.Fprintf(&buf, `Macaroon root="%s", discharge="%s"`, serializedMacaroon, serializedDischarge)
return buf.String()
}
func (t *remoteRepoTestSuite) TestDownloadOK(c *C) {
expectedContent := []byte("I was downloaded")
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
c.Check(url, Equals, "anon-url")
w.Write(expectedContent)
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Size = int64(len(expectedContent))
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
defer os.Remove(path)
content, err := ioutil.ReadFile(path)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, string(expectedContent))
}
func (t *remoteRepoTestSuite) TestDownloadRangeRequest(c *C) {
partialContentStr := "partial content "
missingContentStr := "was downloaded"
expectedContentStr := partialContentStr + missingContentStr
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
c.Check(resume, Equals, int64(len(partialContentStr)))
c.Check(url, Equals, "anon-url")
w.Write([]byte(missingContentStr))
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = "abcdabcd"
snap.Size = int64(len(expectedContentStr))
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
c.Assert(err, IsNil)
err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
content, err := ioutil.ReadFile(targetFn)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, expectedContentStr)
}
func (t *remoteRepoTestSuite) TestResumeOfCompleted(c *C) {
expectedContentStr := "nothing downloaded"
download = nil
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = fmt.Sprintf("%x", sha3.Sum384([]byte(expectedContentStr)))
snap.Size = int64(len(expectedContentStr))
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := ioutil.WriteFile(targetFn+".partial", []byte(expectedContentStr), 0644)
c.Assert(err, IsNil)
err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
content, err := ioutil.ReadFile(targetFn)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, expectedContentStr)
}
func (t *remoteRepoTestSuite) TestDownloadEOFHandlesResumeHashCorrectly(c *C) {
n := 0
var mockServer *httptest.Server
// our mock download content
buf := make([]byte, 50000)
for i := range buf {
buf[i] = 'x'
}
h := crypto.SHA3_384.New()
io.Copy(h, bytes.NewBuffer(buf))
// raise an EOF shortly before the end
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
if n < 2 {
w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf)))
w.Write(buf[0 : len(buf)-5])
mockServer.CloseClientConnections()
return
}
w.Write(buf[len(buf)-5:])
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = mockServer.URL
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
snap.Size = 50000
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
content, err := ioutil.ReadFile(targetFn)
c.Assert(err, IsNil)
c.Assert(content, DeepEquals, buf)
c.Assert(t.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*")
}
func (t *remoteRepoTestSuite) TestDownloadRetryHashErrorIsFullyRetried(c *C) {
n := 0
var mockServer *httptest.Server
// our mock download content
buf := make([]byte, 50000)
for i := range buf {
buf[i] = 'x'
}
h := crypto.SHA3_384.New()
io.Copy(h, bytes.NewBuffer(buf))
// raise an EOF shortly before the end and send the WRONG content next
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
switch n {
case 1:
w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf)))
w.Write(buf[0 : len(buf)-5])
mockServer.CloseClientConnections()
case 2:
io.WriteString(w, "yyyyy")
case 3:
w.Write(buf)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = mockServer.URL
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
snap.Size = 50000
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
content, err := ioutil.ReadFile(targetFn)
c.Assert(err, IsNil)
c.Assert(content, DeepEquals, buf)
c.Assert(t.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*")
}
func (t *remoteRepoTestSuite) TestResumeOfCompletedRetriedOnHashFailure(c *C) {
var mockServer *httptest.Server
// our mock download content
buf := make([]byte, 50000)
badbuf := make([]byte, 50000)
for i := range buf {
buf[i] = 'x'
badbuf[i] = 'y'
}
h := crypto.SHA3_384.New()
io.Copy(h, bytes.NewBuffer(buf))
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(buf)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = mockServer.URL
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
snap.Size = 50000
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
c.Assert(ioutil.WriteFile(targetFn+".partial", badbuf, 0644), IsNil)
err := t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
content, err := ioutil.ReadFile(targetFn)
c.Assert(err, IsNil)
c.Assert(content, DeepEquals, buf)
c.Assert(t.logbuf.String(), Matches, "(?s).*sha3-384 mismatch.*")
}
func (t *remoteRepoTestSuite) TestDownloadRetryHashErrorIsFullyRetriedOnlyOnce(c *C) {
n := 0
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
io.WriteString(w, "something invalid")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = mockServer.URL
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = "invalid-hash"
snap.Size = int64(len("something invalid"))
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
_, ok := err.(HashError)
c.Assert(ok, Equals, true)
// ensure we only retried once (as these downloads might be big)
c.Assert(n, Equals, 2)
}
func (t *remoteRepoTestSuite) TestDownloadRangeRequestRetryOnHashError(c *C) {
expectedContentStr := "file was downloaded from scratch"
partialContentStr := "partial content "
n := 0
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
n++
if n == 1 {
// force sha3 error on first download
c.Check(resume, Equals, int64(len(partialContentStr)))
return HashError{"foo", "1234", "5678"}
}
w.Write([]byte(expectedContentStr))
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = ""
snap.Size = int64(len(expectedContentStr))
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
c.Assert(err, IsNil)
err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
c.Assert(n, Equals, 2)
content, err := ioutil.ReadFile(targetFn)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, expectedContentStr)
}
func (t *remoteRepoTestSuite) TestDownloadRangeRequestFailOnHashError(c *C) {
partialContentStr := "partial content "
n := 0
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
n++
return HashError{"foo", "1234", "5678"}
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Sha3_384 = ""
snap.Size = int64(len(partialContentStr) + 1)
targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
c.Assert(err, IsNil)
err = t.store.Download(context.TODO(), "foo", targetFn, &snap.DownloadInfo, nil, nil)
c.Assert(err, NotNil)
c.Assert(err, ErrorMatches, `sha3-384 mismatch for "foo": got 1234 but expected 5678`)
c.Assert(n, Equals, 2)
}
func (t *remoteRepoTestSuite) TestAuthenticatedDownloadDoesNotUseAnonURL(c *C) {
expectedContent := []byte("I was downloaded")
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
// check user is pass and auth url is used
c.Check(user, Equals, t.user)
c.Check(url, Equals, "AUTH-URL")
w.Write(expectedContent)
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Size = int64(len(expectedContent))
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, t.user)
c.Assert(err, IsNil)
defer os.Remove(path)
content, err := ioutil.ReadFile(path)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, string(expectedContent))
}
func (t *remoteRepoTestSuite) TestAuthenticatedDeviceDoesNotUseAnonURL(c *C) {
expectedContent := []byte("I was downloaded")
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
// check auth url is used
c.Check(url, Equals, "AUTH-URL")
w.Write(expectedContent)
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Size = int64(len(expectedContent))
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&Config{}, authContext)
c.Assert(repo, NotNil)
path := filepath.Join(c.MkDir(), "downloaded-file")
err := repo.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
defer os.Remove(path)
content, err := ioutil.ReadFile(path)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, string(expectedContent))
}
func (t *remoteRepoTestSuite) TestLocalUserDownloadUsesAnonURL(c *C) {
expectedContentStr := "I was downloaded"
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
c.Check(url, Equals, "anon-url")
w.Write([]byte(expectedContentStr))
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Size = int64(len(expectedContentStr))
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, t.localUser)
c.Assert(err, IsNil)
defer os.Remove(path)
content, err := ioutil.ReadFile(path)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, expectedContentStr)
}
func (t *remoteRepoTestSuite) TestDownloadFails(c *C) {
var tmpfile *os.File
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
tmpfile = w.(*os.File)
return fmt.Errorf("uh, it failed")
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Size = 1
// simulate a failed download
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
c.Assert(err, ErrorMatches, "uh, it failed")
// ... and ensure that the tempfile is removed
c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
}
func (t *remoteRepoTestSuite) TestDownloadSyncFails(c *C) {
var tmpfile *os.File
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
tmpfile = w.(*os.File)
w.Write([]byte("sync will fail"))
err := tmpfile.Close()
c.Assert(err, IsNil)
return nil
}
snap := &snap.Info{}
snap.RealName = "foo"
snap.AnonDownloadURL = "anon-url"
snap.DownloadURL = "AUTH-URL"
snap.Size = int64(len("sync will fail"))
// simulate a failed sync
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
c.Assert(err, ErrorMatches, `(sync|fsync:) .*`)
// ... and ensure that the tempfile is removed
c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
}
func (t *remoteRepoTestSuite) TestActualDownload(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
io.WriteString(w, "response-data")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
var buf SillyBuffer
// keep tests happy
sha3 := ""
err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil)
c.Assert(err, IsNil)
c.Check(buf.String(), Equals, "response-data")
c.Check(n, Equals, 1)
}
func (t *remoteRepoTestSuite) TestDownloadCancellation(c *C) {
// the channel used by mock server to request cancellation from the test
syncCh := make(chan struct{})
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
io.WriteString(w, "foo")
syncCh <- struct{}{}
io.WriteString(w, "bar")
time.Sleep(time.Duration(1) * time.Second)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
ctx, cancel := context.WithCancel(context.Background())
result := make(chan string)
go func() {
sha3 := ""
var buf SillyBuffer
err := download(ctx, "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil)
result <- err.Error()
close(result)
}()
<-syncCh
cancel()
err := <-result
c.Check(n, Equals, 1)
c.Assert(err, Equals, "The download has been cancelled: context canceled")
}
type nopeSeeker struct{ io.ReadWriter }
func (nopeSeeker) Seek(int64, int) (int64, error) {
return -1, errors.New("what is this, quidditch?")
}
func (t *remoteRepoTestSuite) TestActualDownloadNonPurchased402(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
// XXX: the server doesn't behave correctly ATM
// but 401 for paid snaps is the unlikely case so far
w.WriteHeader(402)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
var buf bytes.Buffer
err := download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, nopeSeeker{&buf}, -1, nil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "please buy foo before installing it.")
c.Check(n, Equals, 1)
}
func (t *remoteRepoTestSuite) TestActualDownload404(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
w.WriteHeader(404)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
var buf SillyBuffer
err := download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil)
c.Assert(err, NotNil)
c.Assert(err, FitsTypeOf, &DownloadError{})
c.Check(err.(*DownloadError).Code, Equals, 404)
c.Check(n, Equals, 1)
}
func (t *remoteRepoTestSuite) TestActualDownload500(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
w.WriteHeader(500)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
var buf SillyBuffer
err := download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil)
c.Assert(err, NotNil)
c.Assert(err, FitsTypeOf, &DownloadError{})
c.Check(err.(*DownloadError).Code, Equals, 500)
c.Check(n, Equals, 5)
}
func (t *remoteRepoTestSuite) TestActualDownload500Once(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
if n == 1 {
w.WriteHeader(500)
} else {
io.WriteString(w, "response-data")
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
var buf SillyBuffer
// keep tests happy
sha3 := ""
err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil)
c.Assert(err, IsNil)
c.Check(buf.String(), Equals, "response-data")
c.Check(n, Equals, 2)
}
// SillyBuffer is a ReadWriteSeeker buffer with a limited size for the tests
// (bytes does not implement an ReadWriteSeeker)
type SillyBuffer struct {
buf [1024]byte
pos int64
end int64
}
func NewSillyBufferString(s string) *SillyBuffer {
sb := &SillyBuffer{
pos: int64(len(s)),
end: int64(len(s)),
}
copy(sb.buf[0:], []byte(s))
return sb
}
func (sb *SillyBuffer) Read(b []byte) (n int, err error) {
if sb.pos >= int64(sb.end) {
return 0, io.EOF
}
n = copy(b, sb.buf[sb.pos:sb.end])
sb.pos += int64(n)
return n, nil
}
func (sb *SillyBuffer) Seek(offset int64, whence int) (int64, error) {
if whence != 0 {
panic("only io.SeekStart implemented in SillyBuffer")
}
if offset < 0 || offset > int64(sb.end) {
return 0, fmt.Errorf("seek out of bounds: %d", offset)
}
sb.pos = offset
return sb.pos, nil
}
func (sb *SillyBuffer) Write(p []byte) (n int, err error) {
n = copy(sb.buf[sb.pos:], p)
sb.pos += int64(n)
if sb.pos > sb.end {
sb.end = sb.pos
}
return n, nil
}
func (sb *SillyBuffer) String() string {
return string(sb.buf[0:sb.pos])
}
func (t *remoteRepoTestSuite) TestActualDownloadResume(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
n++
io.WriteString(w, "data")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
theStore := New(&Config{}, nil)
buf := NewSillyBufferString("some ")
// calc the expected hash
h := crypto.SHA3_384.New()
h.Write([]byte("some data"))
sha3 := fmt.Sprintf("%x", h.Sum(nil))
err := download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil)
c.Check(err, IsNil)
c.Check(buf.String(), Equals, "some data")
c.Check(n, Equals, 1)
}
func (t *remoteRepoTestSuite) TestUseDeltas(c *C) {
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
restore := release.MockOnClassic(false)
defer restore()
altPath := c.MkDir()
origSnapMountDir := dirs.SnapMountDir
defer func() { dirs.SnapMountDir = origSnapMountDir }()
dirs.SnapMountDir = c.MkDir()
exeInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/usr/bin/xdelta3")
os.MkdirAll(filepath.Dir(exeInCorePath), 0755)
scenarios := []struct {
env string
classic bool
exeInHost bool
exeInCore bool
wantDelta bool
}{
{env: "", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
{env: "", classic: false, exeInHost: false, exeInCore: true, wantDelta: false},
{env: "", classic: false, exeInHost: true, exeInCore: false, wantDelta: false},
{env: "", classic: false, exeInHost: true, exeInCore: true, wantDelta: false},
{env: "", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
{env: "", classic: true, exeInHost: false, exeInCore: true, wantDelta: true},
{env: "", classic: true, exeInHost: true, exeInCore: false, wantDelta: true},
{env: "", classic: true, exeInHost: true, exeInCore: true, wantDelta: true},
{env: "0", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
{env: "0", classic: false, exeInHost: false, exeInCore: true, wantDelta: false},
{env: "0", classic: false, exeInHost: true, exeInCore: false, wantDelta: false},
{env: "0", classic: false, exeInHost: true, exeInCore: true, wantDelta: false},
{env: "0", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
{env: "0", classic: true, exeInHost: false, exeInCore: true, wantDelta: false},
{env: "0", classic: true, exeInHost: true, exeInCore: false, wantDelta: false},
{env: "0", classic: true, exeInHost: true, exeInCore: true, wantDelta: false},
{env: "1", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
{env: "1", classic: false, exeInHost: false, exeInCore: true, wantDelta: true},
{env: "1", classic: false, exeInHost: true, exeInCore: false, wantDelta: true},
{env: "1", classic: false, exeInHost: true, exeInCore: true, wantDelta: true},
{env: "1", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
{env: "1", classic: true, exeInHost: false, exeInCore: true, wantDelta: true},
{env: "1", classic: true, exeInHost: true, exeInCore: false, wantDelta: true},
{env: "1", classic: true, exeInHost: true, exeInCore: true, wantDelta: true},
}
for _, scenario := range scenarios {
if scenario.exeInCore {
osutil.CopyFile("/bin/true", exeInCorePath, 0)
} else {
os.Remove(exeInCorePath)
}
os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", scenario.env)
release.MockOnClassic(scenario.classic)
if scenario.exeInHost {
os.Setenv("PATH", origPath)
} else {
os.Setenv("PATH", altPath)
}
c.Check(useDeltas(), Equals, scenario.wantDelta, Commentf("%#v", scenario))
}
}
type downloadBehaviour []struct {
url string
error bool
}
var deltaTests = []struct {
downloads downloadBehaviour
info snap.DownloadInfo
expectedContent string
}{{
// The full snap is not downloaded, but rather the delta
// is downloaded and applied.
downloads: downloadBehaviour{
{url: "delta-url"},
},
info: snap.DownloadInfo{
AnonDownloadURL: "full-snap-url",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "delta-url", Format: "xdelta3"},
},
},
expectedContent: "snap-content-via-delta",
}, {
// If there is an error during the delta download, the
// full snap is downloaded as per normal.
downloads: downloadBehaviour{
{error: true},
{url: "full-snap-url"},
},
info: snap.DownloadInfo{
AnonDownloadURL: "full-snap-url",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "delta-url", Format: "xdelta3"},
},
},
expectedContent: "full-snap-url-content",
}, {
// If more than one matching delta is returned by the store
// we ignore deltas and do the full download.
downloads: downloadBehaviour{
{url: "full-snap-url"},
},
info: snap.DownloadInfo{
AnonDownloadURL: "full-snap-url",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "delta-url", Format: "xdelta3"},
{AnonDownloadURL: "delta-url-2", Format: "xdelta3"},
},
},
expectedContent: "full-snap-url-content",
}}
func (t *remoteRepoTestSuite) TestDownloadWithDelta(c *C) {
origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
for _, testCase := range deltaTests {
testCase.info.Size = int64(len(testCase.expectedContent))
downloadIndex := 0
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
if testCase.downloads[downloadIndex].error {
downloadIndex++
return errors.New("Bang")
}
c.Check(url, Equals, testCase.downloads[downloadIndex].url)
w.Write([]byte(testCase.downloads[downloadIndex].url + "-content"))
downloadIndex++
return nil
}
applyDelta = func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error {
c.Check(deltaInfo, Equals, &testCase.info.Deltas[0])
err := ioutil.WriteFile(targetPath, []byte("snap-content-via-delta"), 0644)
c.Assert(err, IsNil)
return nil
}
path := filepath.Join(c.MkDir(), "subdir", "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &testCase.info, nil, nil)
c.Assert(err, IsNil)
defer os.Remove(path)
content, err := ioutil.ReadFile(path)
c.Assert(err, IsNil)
c.Assert(string(content), Equals, testCase.expectedContent)
}
}
var downloadDeltaTests = []struct {
info snap.DownloadInfo
authenticated bool
deviceSession bool
useLocalUser bool
format string
expectedURL string
expectError bool
}{{
// An unauthenticated request downloads the anonymous delta url.
info: snap.DownloadInfo{
Sha3_384: "sha3",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
},
},
authenticated: false,
deviceSession: false,
format: "xdelta3",
expectedURL: "anon-delta-url",
expectError: false,
}, {
// An authenticated request downloads the authenticated delta url.
info: snap.DownloadInfo{
Sha3_384: "sha3",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "anon-delta-url", DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
},
},
authenticated: true,
deviceSession: false,
useLocalUser: false,
format: "xdelta3",
expectedURL: "auth-delta-url",
expectError: false,
}, {
// A device-authenticated request downloads the authenticated delta url.
info: snap.DownloadInfo{
Sha3_384: "sha3",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "anon-delta-url", DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
},
},
authenticated: false,
deviceSession: true,
useLocalUser: false,
format: "xdelta3",
expectedURL: "auth-delta-url",
expectError: false,
}, {
// A local authenticated request downloads the anonymous delta url.
info: snap.DownloadInfo{
Sha3_384: "sha3",
Deltas: []snap.DeltaInfo{
{AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
},
},
authenticated: true,
deviceSession: false,
useLocalUser: true,
format: "xdelta3",
expectedURL: "anon-delta-url",
expectError: false,
}, {
// An error is returned if more than one matching delta is returned by the store,
// though this may be handled in the future.
info: snap.DownloadInfo{
Sha3_384: "sha3",
Deltas: []snap.DeltaInfo{
{DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 25},
{DownloadURL: "bsdiff-delta-url", Format: "xdelta3", FromRevision: 25, ToRevision: 26},
},
},
authenticated: false,
deviceSession: false,
format: "xdelta3",
expectedURL: "",
expectError: true,
}, {
// If the supported format isn't available, an error is returned.
info: snap.DownloadInfo{
Sha3_384: "sha3",
Deltas: []snap.DeltaInfo{
{DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
{DownloadURL: "ydelta-delta-url", Format: "ydelta", FromRevision: 24, ToRevision: 26},
},
},
authenticated: false,
deviceSession: false,
format: "bsdiff",
expectedURL: "",
expectError: true,
}}
func (t *remoteRepoTestSuite) TestDownloadDelta(c *C) {
origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
authContext := &testAuthContext{c: c}
repo := New(nil, authContext)
for _, testCase := range downloadDeltaTests {
repo.deltaFormat = testCase.format
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
expectedUser := t.user
if testCase.useLocalUser {
expectedUser = t.localUser
}
if !testCase.authenticated {
expectedUser = nil
}
c.Check(user, Equals, expectedUser)
c.Check(url, Equals, testCase.expectedURL)
w.Write([]byte("I was downloaded"))
return nil
}
w, err := ioutil.TempFile("", "")
c.Assert(err, IsNil)
defer os.Remove(w.Name())
authContext.device = nil
if testCase.deviceSession {
authContext.device = t.device
}
authedUser := t.user
if testCase.useLocalUser {
authedUser = t.localUser
}
if !testCase.authenticated {
authedUser = nil
}
err = repo.downloadDelta("snapname", &testCase.info, w, nil, authedUser)
if testCase.expectError {
c.Assert(err, NotNil)
} else {
c.Assert(err, IsNil)
content, err := ioutil.ReadFile(w.Name())
c.Assert(err, IsNil)
c.Assert(string(content), Equals, "I was downloaded")
}
}
}
var applyDeltaTests = []struct {
deltaInfo snap.DeltaInfo
currentRevision uint
error string
}{{
// A supported delta format can be applied.
deltaInfo: snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26},
currentRevision: 24,
error: "",
}, {
// An error is returned if the expected current snap does not exist on disk.
deltaInfo: snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26},
currentRevision: 23,
error: "snap \"foo\" revision 24 not found",
}, {
// An error is returned if the format is not supported.
deltaInfo: snap.DeltaInfo{Format: "nodelta", FromRevision: 24, ToRevision: 26},
currentRevision: 24,
error: "cannot apply unsupported delta format \"nodelta\" (only xdelta3 currently)",
}}
func (t *remoteRepoTestSuite) TestApplyDelta(c *C) {
for _, testCase := range applyDeltaTests {
name := "foo"
currentSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.currentRevision)
currentSnapPath := filepath.Join(dirs.SnapBlobDir, currentSnapName)
targetSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.deltaInfo.ToRevision)
targetSnapPath := filepath.Join(dirs.SnapBlobDir, targetSnapName)
err := os.MkdirAll(filepath.Dir(currentSnapPath), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(currentSnapPath, nil, 0644)
c.Assert(err, IsNil)
deltaPath := filepath.Join(dirs.SnapBlobDir, "the.delta")
err = ioutil.WriteFile(deltaPath, nil, 0644)
c.Assert(err, IsNil)
// When testing a case where the call to the external
// xdelta3 is successful,
// simulate the resulting .partial.
if testCase.error == "" {
err = ioutil.WriteFile(targetSnapPath+".partial", nil, 0644)
c.Assert(err, IsNil)
}
err = applyDelta(name, deltaPath, &testCase.deltaInfo, targetSnapPath, "")
if testCase.error == "" {
c.Assert(err, IsNil)
c.Assert(t.mockXDelta.Calls(), DeepEquals, [][]string{
{"xdelta3", "-d", "-s", currentSnapPath, deltaPath, targetSnapPath + ".partial"},
})
c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false)
c.Assert(osutil.FileExists(targetSnapPath), Equals, true)
c.Assert(os.Remove(targetSnapPath), IsNil)
} else {
c.Assert(err, NotNil)
c.Assert(err.Error()[0:len(testCase.error)], Equals, testCase.error)
c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false)
c.Assert(osutil.FileExists(targetSnapPath), Equals, false)
}
c.Assert(os.Remove(currentSnapPath), IsNil)
c.Assert(os.Remove(deltaPath), IsNil)
}
}
var (
userAgent = httputil.UserAgent()
)
func (t *remoteRepoTestSuite) TestDoRequestSetsAuth(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
// check user authorization is set
authorization := r.Header.Get("Authorization")
c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
// check device authorization is set
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
io.WriteString(w, "response-data")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
repo := New(&Config{}, authContext)
c.Assert(repo, NotNil)
endpoint, _ := url.Parse(mockServer.URL)
reqOptions := &requestOptions{Method: "GET", URL: endpoint}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
defer response.Body.Close()
c.Assert(err, IsNil)
responseData, err := ioutil.ReadAll(response.Body)
c.Assert(err, IsNil)
c.Check(string(responseData), Equals, "response-data")
}
func (t *remoteRepoTestSuite) TestDoRequestDoesNotSetAuthForLocalOnlyUser(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
// check no user authorization is set
authorization := r.Header.Get("Authorization")
c.Check(authorization, Equals, "")
// check device authorization is set
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
io.WriteString(w, "response-data")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
authContext := &testAuthContext{c: c, device: t.device, user: t.localUser}
repo := New(&Config{}, authContext)
c.Assert(repo, NotNil)
endpoint, _ := url.Parse(mockServer.URL)
reqOptions := &requestOptions{Method: "GET", URL: endpoint}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.localUser)
defer response.Body.Close()
c.Assert(err, IsNil)
responseData, err := ioutil.ReadAll(response.Body)
c.Assert(err, IsNil)
c.Check(string(responseData), Equals, "response-data")
}
func (t *remoteRepoTestSuite) TestDoRequestAuthNoSerial(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
// check user authorization is set
authorization := r.Header.Get("Authorization")
c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
// check device authorization was not set
c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
io.WriteString(w, "response-data")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
// no serial and no device macaroon => no device auth
t.device.Serial = ""
t.device.SessionMacaroon = ""
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
repo := New(&Config{}, authContext)
c.Assert(repo, NotNil)
endpoint, _ := url.Parse(mockServer.URL)
reqOptions := &requestOptions{Method: "GET", URL: endpoint}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
defer response.Body.Close()
c.Assert(err, IsNil)
responseData, err := ioutil.ReadAll(response.Body)
c.Assert(err, IsNil)
c.Check(string(responseData), Equals, "response-data")
}
func (t *remoteRepoTestSuite) TestDoRequestRefreshesAuth(c *C) {
refresh, err := makeTestRefreshDischargeResponse()
c.Assert(err, IsNil)
c.Check(t.user.StoreDischarges[0], Not(Equals), refresh)
// mock refresh response
refreshDischargeEndpointHit := false
mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, refresh))
refreshDischargeEndpointHit = true
}))
defer mockSSOServer.Close()
UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
// mock store response (requiring auth refresh)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
authorization := r.Header.Get("Authorization")
c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
if t.user.StoreDischarges[0] == refresh {
io.WriteString(w, "response-data")
} else {
w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
w.WriteHeader(401)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
repo := New(&Config{}, authContext)
c.Assert(repo, NotNil)
endpoint, _ := url.Parse(mockServer.URL)
reqOptions := &requestOptions{Method: "GET", URL: endpoint}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
defer response.Body.Close()
c.Assert(err, IsNil)
responseData, err := ioutil.ReadAll(response.Body)
c.Assert(err, IsNil)
c.Check(string(responseData), Equals, "response-data")
c.Check(refreshDischargeEndpointHit, Equals, true)
}
func (t *remoteRepoTestSuite) TestDoRequestForwardsRefreshAuthFailure(c *C) {
// mock refresh response
refreshDischargeEndpointHit := false
mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(mockStoreInvalidLoginCode)
io.WriteString(w, mockStoreInvalidLogin)
refreshDischargeEndpointHit = true
}))
defer mockSSOServer.Close()
UbuntuoneRefreshDischargeAPI = mockSSOServer.URL + "/tokens/refresh"
// mock store response (requiring auth refresh)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
authorization := r.Header.Get("Authorization")
c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
w.Header().Set("WWW-Authenticate", "Macaroon needs_refresh=1")
w.WriteHeader(401)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
repo := New(&Config{}, authContext)
c.Assert(repo, NotNil)
endpoint, _ := url.Parse(mockServer.URL)
reqOptions := &requestOptions{Method: "GET", URL: endpoint}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
c.Assert(err, Equals, ErrInvalidCredentials)
c.Check(response, IsNil)
c.Check(refreshDischargeEndpointHit, Equals, true)
}
func (t *remoteRepoTestSuite) TestDoRequestSetsAndRefreshesDeviceAuth(c *C) {
deviceSessionRequested := false
refreshSessionRequested := false
expiredAuth := `Macaroon root="expired-session-macaroon"`
// mock store response
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, userAgent)
switch r.URL.Path {
case "/":
authorization := r.Header.Get("X-Device-Authorization")
if authorization == "" {
c.Fatalf("device authentication missing")
} else if authorization == expiredAuth {
w.Header().Set("WWW-Authenticate", "Macaroon refresh_device_session=1")
w.WriteHeader(401)
} else {
c.Check(authorization, Equals, `Macaroon root="refreshed-session-macaroon"`)
io.WriteString(w, "response-data")
}
case authNoncesPath:
io.WriteString(w, `{"nonce": "1234567890:9876543210"}`)
case authSessionPath:
// sanity of request
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var req map[string]string
err = json.Unmarshal(jsonReq, &req)
c.Assert(err, IsNil)
c.Check(strings.HasPrefix(req["device-session-request"], "type: device-session-request\n"), Equals, true)
c.Check(strings.HasPrefix(req["serial-assertion"], "type: serial\n"), Equals, true)
c.Check(strings.HasPrefix(req["model-assertion"], "type: model\n"), Equals, true)
authorization := r.Header.Get("X-Device-Authorization")
if authorization == "" {
io.WriteString(w, `{"macaroon": "expired-session-macaroon"}`)
deviceSessionRequested = true
} else {
c.Check(authorization, Equals, expiredAuth)
io.WriteString(w, `{"macaroon": "refreshed-session-macaroon"}`)
refreshSessionRequested = true
}
default:
c.Fatalf("unexpected path %q", r.URL.Path)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
// make sure device session is not set
t.device.SessionMacaroon = ""
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
repo := New(&Config{
StoreBaseURL: mockServerURL,
}, authContext)
c.Assert(repo, NotNil)
reqOptions := &requestOptions{Method: "GET", URL: mockServerURL}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
c.Assert(err, IsNil)
defer response.Body.Close()
responseData, err := ioutil.ReadAll(response.Body)
c.Assert(err, IsNil)
c.Check(string(responseData), Equals, "response-data")
c.Check(deviceSessionRequested, Equals, true)
c.Check(refreshSessionRequested, Equals, true)
}
func (t *remoteRepoTestSuite) TestDoRequestSetsExtraHeaders(c *C) {
// Custom headers are applied last.
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.UserAgent(), Equals, `customAgent`)
c.Check(r.Header.Get("X-Foo-Header"), Equals, `Bar`)
c.Check(r.Header.Get("Content-Type"), Equals, `application/bson`)
c.Check(r.Header.Get("Accept"), Equals, `application/hal+bson`)
io.WriteString(w, "response-data")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
repo := New(&Config{}, nil)
c.Assert(repo, NotNil)
endpoint, _ := url.Parse(mockServer.URL)
reqOptions := &requestOptions{
Method: "GET",
URL: endpoint,
ExtraHeaders: map[string]string{
"X-Foo-Header": "Bar",
"Content-Type": "application/bson",
"Accept": "application/hal+bson",
"User-Agent": "customAgent",
},
}
response, err := repo.doRequest(context.TODO(), repo.client, reqOptions, t.user)
defer response.Body.Close()
c.Assert(err, IsNil)
responseData, err := ioutil.ReadAll(response.Body)
c.Assert(err, IsNil)
c.Check(string(responseData), Equals, "response-data")
}
func (t *remoteRepoTestSuite) TestLoginUser(c *C) {
macaroon, err := makeTestMacaroon()
c.Assert(err, IsNil)
serializedMacaroon, err := auth.MacaroonSerialize(macaroon)
c.Assert(err, IsNil)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
io.WriteString(w, fmt.Sprintf(`{"macaroon": "%s"}`, serializedMacaroon))
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
discharge, err := makeTestDischarge()
c.Assert(err, IsNil)
serializedDischarge, err := auth.MacaroonSerialize(discharge)
c.Assert(err, IsNil)
mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
io.WriteString(w, fmt.Sprintf(`{"discharge_macaroon": "%s"}`, serializedDischarge))
}))
c.Assert(mockSSOServer, NotNil)
defer mockSSOServer.Close()
UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge"
userMacaroon, userDischarge, err := LoginUser("username", "password", "otp")
c.Assert(err, IsNil)
c.Check(userMacaroon, Equals, serializedMacaroon)
c.Check(userDischarge, Equals, serializedDischarge)
}
func (t *remoteRepoTestSuite) TestLoginUserMyAppsError(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
io.WriteString(w, "{}")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
userMacaroon, userDischarge, err := LoginUser("username", "password", "otp")
c.Assert(err, ErrorMatches, "cannot get snap access permission from store: .*")
c.Check(userMacaroon, Equals, "")
c.Check(userDischarge, Equals, "")
}
func (t *remoteRepoTestSuite) TestLoginUserSSOError(c *C) {
macaroon, err := makeTestMacaroon()
c.Assert(err, IsNil)
serializedMacaroon, err := auth.MacaroonSerialize(macaroon)
c.Assert(err, IsNil)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
io.WriteString(w, fmt.Sprintf(`{"macaroon": "%s"}`, serializedMacaroon))
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
MyAppsMacaroonACLAPI = mockServer.URL + "/acl/"
errorResponse := `{"code": "some-error"}`
mockSSOServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(401)
io.WriteString(w, errorResponse)
}))
c.Assert(mockSSOServer, NotNil)
defer mockSSOServer.Close()
UbuntuoneDischargeAPI = mockSSOServer.URL + "/tokens/discharge"
userMacaroon, userDischarge, err := LoginUser("username", "password", "otp")
c.Assert(err, ErrorMatches, "cannot authenticate to snap store: .*")
c.Check(userMacaroon, Equals, "")
c.Check(userDischarge, Equals, "")
}
const (
funkyAppName = "8nzc1x4iim2xj1g2ul64"
funkyAppDeveloper = "chipaca"
funkyAppSnapID = "1e21e12ex4iim2xj1g2ul6f12f1"
helloWorldSnapID = "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ"
helloWorldDeveloperID = "canonical"
)
/* acquired via
http --pretty=format --print b https://api.snapcraft.io/api/v1/snaps/details/hello-world X-Ubuntu-Series:16 fields==anon_download_url,architecture,channel,download_sha3_384,summary,description,binary_filesize,download_url,icon_url,last_updated,license,package_name,prices,publisher,ratings_average,revision,screenshot_urls,snap_id,support_url,title,content,version,origin,developer_id,private,confinement channel==edge | xsel -b
on 2016-07-03. Then, by hand:
* set prices to {"EUR": 0.99, "USD": 1.23}.
* Screenshot URLS set manually.
On Ubuntu, apt install httpie xsel (although you could get http from
the http snap instead).
*/
const MockDetailsJSON = `{
"_links": {
"self": {
"href": "https://api.snapcraft.io/api/v1/snaps/details/hello-world?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement&channel=edge"
}
},
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
"architecture": [
"all"
],
"base": "bare-base",
"binary_filesize": 20480,
"channel": "edge",
"confinement": "strict",
"content": "application",
"description": "This is a simple hello world example.",
"developer_id": "canonical",
"download_sha3_384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
"icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"last_updated": "2016-07-12T16:37:23.960632Z",
"license": "GPL-3.0",
"origin": "canonical",
"package_name": "hello-world",
"prices": {"EUR": 0.99, "USD": 1.23},
"publisher": "Canonical",
"ratings_average": 0.0,
"revision": 27,
"screenshot_urls": ["https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png"],
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"summary": "The 'hello-world' of snaps",
"support_url": "mailto:snappy-devel@lists.ubuntu.com",
"title": "Hello World",
"version": "6.3",
"channel_maps_list": [
{
"track": "latest",
"map": [
{
"info": "released",
"version": "v1",
"binary_filesize": 12345,
"epoch": "0",
"confinement": "strict",
"channel": "stable",
"revision": 1
},
{
"info": "released",
"version": "v2",
"binary_filesize": 12345,
"epoch": "0",
"confinement": "strict",
"channel": "candidate",
"revision": 2
},
{
"info": "released",
"version": "v8",
"binary_filesize": 12345,
"epoch": "0",
"confinement": "devmode",
"channel": "beta",
"revision": 8
},
{
"info": "released",
"version": "v9",
"binary_filesize": 12345,
"epoch": "0",
"confinement": "devmode",
"channel": "edge",
"revision": 9
}
]
}
]
}
`
// FIXME: this can go once the store always provides a channel_map_list
const MockDetailsJSONnoChannelMapList = `{
"_links": {
"self": {
"href": "https://api.snapcraft.io/api/v1/snaps/details/hello-world?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha3_384%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin%2Cdeveloper_id%2Cprivate%2Cconfinement&channel=edge"
}
},
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
"architecture": [
"all"
],
"binary_filesize": 20480,
"channel": "edge",
"confinement": "strict",
"content": "application",
"description": "This is a simple hello world example.",
"developer_id": "canonical",
"download_sha3_384": "eed62063c04a8c3819eb71ce7d929cc8d743b43be9e7d86b397b6d61b66b0c3a684f3148a9dbe5821360ae32105c1bd9",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_27.snap",
"icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"last_updated": "2016-07-12T16:37:23.960632Z",
"license": "GPL-3.0",
"origin": "canonical",
"package_name": "hello-world",
"prices": {"EUR": 0.99, "USD": 1.23},
"publisher": "Canonical",
"ratings_average": 0.0,
"revision": 27,
"screenshot_urls": ["https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png"],
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"summary": "The 'hello-world' of snaps",
"support_url": "mailto:snappy-devel@lists.ubuntu.com",
"title": "Hello World",
"version": "6.3"
}
`
const mockOrdersJSON = `{
"orders": [
{
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"currency": "USD",
"amount": "1.99",
"state": "Complete",
"refundable_until": "2015-07-15 18:46:21",
"purchase_date": "2016-09-20T15:00:00+00:00"
},
{
"snap_id": "1e21e12ex4iim2xj1g2ul6f12f1",
"currency": "USD",
"amount": "1.99",
"state": "Complete",
"refundable_until": "2015-07-17 11:33:29",
"purchase_date": "2016-09-20T15:00:00+00:00"
}
]
}`
const mockOrderResponseJSON = `{
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"currency": "USD",
"amount": "1.99",
"state": "Complete",
"refundable_until": "2015-07-15 18:46:21",
"purchase_date": "2016-09-20T15:00:00+00:00"
}`
const mockSingleOrderJSON = `{
"orders": [
{
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"currency": "USD",
"amount": "1.99",
"state": "Complete",
"refundable_until": "2015-07-15 18:46:21",
"purchase_date": "2016-09-20T15:00:00+00:00"
}
]
}`
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails(c *C) {
restore := release.MockOnClassic(false)
defer restore()
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
c.Check(r.UserAgent(), Equals, userAgent)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
// no store ID by default
storeID := r.Header.Get("X-Ubuntu-Store")
c.Check(storeID, Equals, "")
c.Check(r.URL.Path, Matches, ".*/hello-world")
c.Check(r.URL.Query().Get("channel"), Equals, "edge")
c.Check(r.URL.Query().Get("fields"), Equals, "abc,def")
c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, release.Series)
c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, arch.UbuntuArchitecture())
c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "false")
c.Check(r.Header.Get("X-Ubuntu-Confinement"), Equals, "")
w.Header().Set("X-Suggested-Currency", "GBP")
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{"abc", "def"},
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
c.Check(result.Architectures, DeepEquals, []string{"all"})
c.Check(result.Revision, Equals, snap.R(27))
c.Check(result.SnapID, Equals, helloWorldSnapID)
c.Check(result.Publisher, Equals, "canonical")
c.Check(result.Version, Equals, "6.3")
c.Check(result.Sha3_384, Matches, `[[:xdigit:]]{96}`)
c.Check(result.Size, Equals, int64(20480))
c.Check(result.Channel, Equals, "edge")
c.Check(result.Description(), Equals, "This is a simple hello world example.")
c.Check(result.Summary(), Equals, "The 'hello-world' of snaps")
c.Check(result.Title(), Equals, "Hello World")
c.Check(result.License, Equals, "GPL-3.0")
c.Assert(result.Prices, DeepEquals, map[string]float64{"EUR": 0.99, "USD": 1.23})
c.Assert(result.Paid, Equals, true)
c.Assert(result.Screenshots, DeepEquals, []snap.ScreenshotInfo{
{
URL: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png",
},
})
c.Check(result.MustBuy, Equals, true)
c.Check(result.Contact, Equals, "mailto:snappy-devel@lists.ubuntu.com")
c.Check(result.Base, Equals, "bare-base")
// Make sure the epoch (currently not sent by the store) defaults to "0"
c.Check(result.Epoch.String(), Equals, "0")
c.Check(repo.SuggestedCurrency(), Equals, "GBP")
// skip this one until the store supports it
// c.Check(result.Private, Equals, true)
c.Check(snap.Validate(result), IsNil)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsDefaultChannelIsStable(c *C) {
restore := release.MockOnClassic(false)
defer restore()
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
c.Check(r.URL.Path, Matches, ".*/hello-world")
c.Check(r.URL.Query().Get("channel"), Equals, "stable")
w.WriteHeader(200)
io.WriteString(w, strings.Replace(MockDetailsJSON, "edge", "stable", -1))
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{"abc", "def"},
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
c.Check(result.SnapID, Equals, helloWorldSnapID)
c.Check(result.Channel, Equals, "stable")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails500(c *C) {
var n = 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
n++
w.WriteHeader(500)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{},
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
_, err := repo.SnapInfo(spec, nil)
c.Assert(err, NotNil)
c.Assert(err, ErrorMatches, `cannot get details for snap "hello-world" in channel "edge": got unexpected HTTP status code 500 via GET to "http://.*?/details/hello-world\?channel=edge"`)
c.Assert(n, Equals, 5)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetails500once(c *C) {
var n = 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
n++
if n > 1 {
w.Header().Set("X-Suggested-Currency", "GBP")
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
} else {
w.WriteHeader(500)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
c.Assert(n, Equals, 2)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsAndChannels(c *C) {
// this test will break and should be melded into TestUbuntuStoreRepositoryDetails,
// above, when the store provides the channels as part of details
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
switch n {
case 0:
c.Check(r.URL.Path, Matches, ".*/hello-world")
c.Check(r.URL.Query().Get("channel"), Equals, "")
w.Header().Set("X-Suggested-Currency", "GBP")
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
default:
c.Fatalf("unexpected request to %q", r.URL.Path)
}
n++
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
AnyChannel: true,
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Assert(n, Equals, 1)
c.Check(result.Name(), Equals, "hello-world")
c.Check(result.Channels, DeepEquals, map[string]*snap.ChannelSnapInfo{
"latest/stable": {
Revision: snap.R(1),
Version: "v1",
Confinement: snap.StrictConfinement,
Channel: "stable",
Size: 12345,
Epoch: *snap.E("0"),
},
"latest/candidate": {
Revision: snap.R(2),
Version: "v2",
Confinement: snap.StrictConfinement,
Channel: "candidate",
Size: 12345,
Epoch: *snap.E("0"),
},
"latest/beta": {
Revision: snap.R(8),
Version: "v8",
Confinement: snap.DevModeConfinement,
Channel: "beta",
Size: 12345,
Epoch: *snap.E("0"),
},
"latest/edge": {
Revision: snap.R(9),
Version: "v9",
Confinement: snap.DevModeConfinement,
Channel: "edge",
Size: 12345,
Epoch: *snap.E("0"),
},
})
c.Check(snap.Validate(result), IsNil)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNonDefaults(c *C) {
restore := release.MockOnClassic(true)
defer restore()
os.Setenv("SNAPPY_STORE_NO_CDN", "1")
defer os.Unsetenv("SNAPPY_STORE_NO_CDN")
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
storeID := r.Header.Get("X-Ubuntu-Store")
c.Check(storeID, Equals, "foo")
c.Check(r.URL.Path, Matches, ".*/details/hello-world")
c.Check(r.URL.Query().Get("channel"), Equals, "edge")
c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, "21")
c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, "archXYZ")
c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "true")
c.Check(r.Header.Get("X-Ubuntu-No-CDN"), Equals, "true")
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := DefaultConfig()
cfg.StoreBaseURL = mockServerURL
cfg.Series = "21"
cfg.Architecture = "archXYZ"
cfg.StoreID = "foo"
repo := New(cfg, nil)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryStoreIDFromAuthContext(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
storeID := r.Header.Get("X-Ubuntu-Store")
c.Check(storeID, Equals, "my-brand-store-id")
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := DefaultConfig()
cfg.StoreBaseURL = mockServerURL
cfg.Series = "21"
cfg.Architecture = "archXYZ"
cfg.StoreID = "fallback"
repo := New(cfg, &testAuthContext{c: c, device: t.device, storeID: "my-brand-store-id"})
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryProxyStoreFromAuthContext(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
nowhereURL, err := url.Parse("http://nowhere.invalid")
c.Assert(err, IsNil)
cfg := DefaultConfig()
cfg.StoreBaseURL = nowhereURL
repo := New(cfg, &testAuthContext{
c: c,
device: t.device,
proxyStoreID: "foo",
proxyStoreURL: mockServerURL,
})
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryProxyStoreFromAuthContextURLFallback(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := DefaultConfig()
cfg.StoreBaseURL = mockServerURL
repo := New(cfg, &testAuthContext{
c: c,
device: t.device,
// mock an assertion that has id but no url
proxyStoreID: "foo",
proxyStoreURL: nil,
})
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRevision(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ordersPath:
w.WriteHeader(404)
case detailsPath("hello-world"):
c.Check(r.URL.Query(), DeepEquals, url.Values{
"channel": []string{""},
"revision": []string{"26"},
})
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
default:
c.Fatalf("unexpected request to %q", r.URL.Path)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := DefaultConfig()
cfg.StoreBaseURL = mockServerURL
cfg.DetailFields = []string{}
repo := New(cfg, nil)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(26),
}
result, err := repo.SnapInfo(spec, t.user)
c.Assert(err, IsNil)
c.Check(result.Name(), Equals, "hello-world")
c.Check(result.Revision, DeepEquals, snap.R(27))
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDetailsOopses(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
c.Check(r.URL.Path, Matches, ".*/hello-world")
c.Check(r.URL.Query().Get("channel"), Equals, "edge")
w.Header().Set("X-Oops-Id", "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f")
w.WriteHeader(500)
io.WriteString(w, `{"oops": "OOPS-d4f46f75a5bcc10edcacc87e1fc0119f"}`)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
_, err := repo.SnapInfo(spec, nil)
c.Assert(err, ErrorMatches, `cannot get details for snap "hello-world" in channel "edge": got unexpected HTTP status code 5.. via GET to "http://\S+" \[OOPS-[[:xdigit:]]*\]`)
}
/*
acquired via
http --pretty=format --print b https://api.snapcraft.io/api/v1/snaps/details/no:such:package X-Ubuntu-Series:16 fields==anon_download_url,architecture,channel,download_sha512,summary,description,binary_filesize,download_url,icon_url,last_updated,license,package_name,prices,publisher,ratings_average,revision,snap_id,support_url,title,content,version,origin,developer_id,private,confinement channel==edge | xsel -b
on 2016-07-03
On Ubuntu, apt install httpie xsel (although you could get http from
the http snap instead).
*/
const MockNoDetailsJSON = `{
"errors": [
"No such package"
],
"result": "error"
}`
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryNoDetails(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
c.Check(r.URL.Path, Matches, ".*/no-such-pkg")
q := r.URL.Query()
c.Check(q.Get("channel"), Equals, "edge")
w.WriteHeader(404)
io.WriteString(w, MockNoDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
// the actual test
spec := SnapSpec{
Name: "no-such-pkg",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, NotNil)
c.Assert(result, IsNil)
}
func (t *remoteRepoTestSuite) TestStructFields(c *C) {
type s struct {
Foo int `json:"hello"`
Bar int `json:"potato,stuff"`
}
c.Assert(getStructFields(s{}), DeepEquals, []string{"hello", "potato"})
}
/* acquired via:
curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://api.snapcraft.io/api/v1/snaps/search?fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&q=hello' | python -m json.tool | xsel -b
Screenshot URLS set manually.
*/
const MockSearchJSON = `{
"_embedded": {
"clickindex:package": [
{
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25.snap",
"architecture": [
"all"
],
"binary_filesize": 20480,
"channel": "edge",
"content": "application",
"description": "This is a simple hello world example.",
"download_sha512": "4bf23ce93efa1f32f0aeae7ec92564b7b0f9f8253a0bd39b2741219c1be119bb676c21208c6845ccf995e6aabe791d3f28a733ebcbbc3171bb23f67981f4068e",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25.snap",
"icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"last_updated": "2016-04-19T19:50:50.435291Z",
"license": "GPL-3.0",
"origin": "canonical",
"package_name": "hello-world",
"prices": {"EUR": 2.99, "USD": 3.49},
"publisher": "Canonical",
"ratings_average": 0.0,
"revision": 25,
"screenshot_urls": ["https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/screenshot.png"],
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"summary": "Hello world example",
"support_url": "mailto:snappy-devel@lists.ubuntu.com",
"title": "Hello World",
"version": "6.0"
}
]
},
"_links": {
"first": {
"href": "https://api.snapcraft.io/api/v1/snaps/search?q=hello&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&page=1"
},
"last": {
"href": "https://api.snapcraft.io/api/v1/snaps/search?q=hello&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&page=1"
},
"self": {
"href": "https://api.snapcraft.io/api/v1/snaps/search?q=hello&fields=anon_download_url%2Carchitecture%2Cchannel%2Cdownload_sha512%2Csummary%2Cdescription%2Cbinary_filesize%2Cdownload_url%2Cicon_url%2Clast_updated%2Clicense%2Cpackage_name%2Cprices%2Cpublisher%2Cratings_average%2Crevision%2Cscreenshot_urls%2Csnap_id%2Csupport_url%2Ctitle%2Ccontent%2Cversion%2Corigin&page=1"
}
}
}
`
func (t *remoteRepoTestSuite) TestUbuntuStoreFindQueries(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
query := r.URL.Query()
name := query.Get("name")
q := query.Get("q")
section := query.Get("section")
c.Check(r.URL.Path, Matches, ".*/search")
c.Check(query.Get("fields"), Equals, "abc,def")
// write dummy json so that Find doesn't re-try due to json decoder EOF error
io.WriteString(w, "{}")
switch n {
case 0:
c.Check(name, Equals, "hello")
c.Check(q, Equals, "")
c.Check(section, Equals, "")
case 1:
c.Check(name, Equals, "")
c.Check(q, Equals, "hello")
c.Check(section, Equals, "")
case 2:
c.Check(name, Equals, "")
c.Check(q, Equals, "")
c.Check(section, Equals, "db")
case 3:
c.Check(name, Equals, "")
c.Check(q, Equals, "hello")
c.Check(section, Equals, "db")
default:
c.Fatalf("what? %d", n)
}
n++
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{"abc", "def"},
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
for _, query := range []Search{
{Query: "hello", Prefix: true},
{Query: "hello"},
{Section: "db"},
{Query: "hello", Section: "db"},
} {
repo.Find(&query, nil)
}
}
/* acquired via:
curl -s -H "accept: application/hal+json" -H "X-Ubuntu-Release: 16" -H "X-Ubuntu-Device-Channel: edge" -H "X-Ubuntu-Wire-Protocol: 1" -H "X-Ubuntu-Architecture: amd64" 'https://api.snapcraft.io/api/v1/snaps/sections'
*/
const MockSectionsJSON = `{
"_embedded": {
"clickindex:sections": [
{
"name": "featured"
},
{
"name": "database"
}
]
},
"_links": {
"self": {
"href": "http://api.snapcraft.io/api/v1/snaps/sections"
}
}
}
`
func (t *remoteRepoTestSuite) TestUbuntuStoreSectionsQuery(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", sectionsPath)
switch n {
case 0:
// All good.
default:
c.Fatalf("what? %d", n)
}
w.Header().Set("Content-Type", "application/hal+json")
w.WriteHeader(200)
io.WriteString(w, MockSectionsJSON)
n++
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
serverURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: serverURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
sections, err := repo.Sections(t.user)
c.Check(err, IsNil)
c.Check(sections, DeepEquals, []string{"featured", "database"})
}
const mockNamesJSON = `
{
"_embedded": {
"clickindex:package": [
{
"aliases": [
{
"name": "potato",
"target": "baz"
},
{
"name": "meh",
"target": "baz"
}
],
"apps": ["baz"],
"title": "a title",
"summary": "oneary plus twoary",
"package_name": "bar"
},
{
"aliases": [{"name": "meh", "target": "foo"}],
"apps": ["foo"],
"package_name": "foo"
}
]
}
}`
func (t *remoteRepoTestSuite) TestUbuntuStoreSnapCommandsOnClassic(c *C) {
t.testUbuntuStoreSnapCommands(c, true)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreSnapCommandsOnCore(c *C) {
t.testUbuntuStoreSnapCommands(c, false)
}
func (t *remoteRepoTestSuite) testUbuntuStoreSnapCommands(c *C, onClassic bool) {
c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil)
defer release.MockOnClassic(onClassic)()
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
query := r.URL.Query()
c.Check(query, HasLen, 1)
expectedConfinement := "strict"
if onClassic {
expectedConfinement = "strict,classic"
}
c.Check(query.Get("confinement"), Equals, expectedConfinement)
c.Check(r.URL.Path, Equals, "/api/v1/snaps/names")
default:
c.Fatalf("what? %d", n)
}
w.Header().Set("Content-Type", "application/hal+json")
w.Header().Set("Content-Length", fmt.Sprint(len(mockNamesJSON)))
w.WriteHeader(200)
io.WriteString(w, mockNamesJSON)
n++
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
serverURL, _ := url.Parse(mockServer.URL)
repo := New(&Config{StoreBaseURL: serverURL}, nil)
c.Assert(repo, NotNil)
db, err := advisor.Create()
c.Assert(err, IsNil)
defer db.Rollback()
var bufNames bytes.Buffer
err = repo.WriteCatalogs(&bufNames, db)
c.Assert(err, IsNil)
db.Commit()
c.Check(bufNames.String(), Equals, "bar\nfoo\n")
dump, err := advisor.Dump()
c.Assert(err, IsNil)
c.Check(dump, DeepEquals, map[string][]string{
"foo": {"foo"},
"bar.baz": {"bar"},
"potato": {"bar"},
"meh": {"bar", "foo"},
})
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFindPrivate(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
query := r.URL.Query()
name := query.Get("name")
q := query.Get("q")
switch n {
case 0:
c.Check(r.URL.Path, Matches, ".*/search")
c.Check(name, Equals, "")
c.Check(q, Equals, "foo")
c.Check(query.Get("private"), Equals, "true")
default:
c.Fatalf("what? %d", n)
}
w.Header().Set("Content-Type", "application/hal+json")
w.WriteHeader(200)
io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1))
n++
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
serverURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: serverURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
_, err := repo.Find(&Search{Query: "foo", Private: true}, t.user)
c.Check(err, IsNil)
_, err = repo.Find(&Search{Query: "foo", Private: true}, nil)
c.Check(err, Equals, ErrUnauthenticated)
_, err = repo.Find(&Search{Query: "name:foo", Private: true}, t.user)
c.Check(err, Equals, ErrBadQuery)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFindFailures(c *C) {
repo := New(&Config{StoreBaseURL: new(url.URL)}, nil)
_, err := repo.Find(&Search{Query: "foo:bar"}, nil)
c.Check(err, Equals, ErrBadQuery)
_, err = repo.Find(&Search{Query: "foo", Private: true, Prefix: true}, t.user)
c.Check(err, Equals, ErrBadQuery)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFindFails(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
c.Check(r.URL.Query().Get("q"), Equals, "hello")
http.Error(w, http.StatusText(418), 418) // I'm a teapot
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{}, // make the error less noisy
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
snaps, err := repo.Find(&Search{Query: "hello"}, nil)
c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 418 via GET to "http://\S+[?&]q=hello.*"`)
c.Check(snaps, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadContentType(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
c.Check(r.URL.Query().Get("q"), Equals, "hello")
io.WriteString(w, MockSearchJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{}, // make the error less noisy
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
snaps, err := repo.Find(&Search{Query: "hello"}, nil)
c.Check(err, ErrorMatches, `received an unexpected content type \("text/plain[^"]+"\) when trying to search via "http://\S+[?&]q=hello.*"`)
c.Check(snaps, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFindBadBody(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
query := r.URL.Query()
c.Check(query.Get("q"), Equals, "hello")
w.Header().Set("Content-Type", "application/hal+json")
io.WriteString(w, "<hello>")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{}, // make the error less noisy
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
snaps, err := repo.Find(&Search{Query: "hello"}, nil)
c.Check(err, ErrorMatches, `invalid character '<' looking for beginning of value`)
c.Check(snaps, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFind500(c *C) {
var n = 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
n++
w.WriteHeader(500)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{},
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
_, err := repo.Find(&Search{Query: "hello"}, nil)
c.Check(err, ErrorMatches, `cannot search: got unexpected HTTP status code 500 via GET to "http://\S+[?&]q=hello.*"`)
c.Assert(n, Equals, 5)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFind500once(c *C) {
var n = 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", searchPath)
n++
if n == 1 {
w.WriteHeader(500)
} else {
w.Header().Set("Content-Type", "application/hal+json")
w.WriteHeader(200)
io.WriteString(w, strings.Replace(MockSearchJSON, `"EUR": 2.99, "USD": 3.49`, "", -1))
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{},
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
snaps, err := repo.Find(&Search{Query: "hello"}, nil)
c.Check(err, IsNil)
c.Assert(snaps, HasLen, 1)
c.Assert(n, Equals, 2)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreFindAuthFailed(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case searchPath:
// check authorization is set
authorization := r.Header.Get("Authorization")
c.Check(authorization, Equals, t.expectedAuthorization(c, t.user))
query := r.URL.Query()
c.Check(query.Get("q"), Equals, "foo")
if release.OnClassic {
c.Check(query.Get("confinement"), Matches, `strict,classic|classic,strict`)
} else {
c.Check(query.Get("confinement"), Equals, "strict")
}
w.Header().Set("Content-Type", "application/hal+json")
io.WriteString(w, MockSearchJSON)
case ordersPath:
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, ordersPath)
w.WriteHeader(401)
io.WriteString(w, "{}")
default:
c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
DetailFields: []string{}, // make the error less noisy
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
snaps, err := repo.Find(&Search{Query: "foo"}, t.user)
c.Assert(err, IsNil)
// Check that we log an error.
c.Check(t.logbuf.String(), Matches, "(?ms).* cannot get user orders: invalid credentials")
// But still successfully return snap information.
c.Assert(snaps, HasLen, 1)
c.Check(snaps[0].SnapID, Equals, helloWorldSnapID)
c.Check(snaps[0].Prices, DeepEquals, map[string]float64{"EUR": 2.99, "USD": 3.49})
c.Check(snaps[0].MustBuy, Equals, true)
}
func (t *remoteRepoTestSuite) TestCurrentSnap(c *C) {
cand := &RefreshCandidate{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
Epoch: *snap.E("1"),
}
cs := currentSnap(cand)
c.Assert(cs, NotNil)
c.Check(cs.SnapID, Equals, cand.SnapID)
c.Check(cs.Channel, Equals, cand.Channel)
c.Check(cs.Epoch, DeepEquals, cand.Epoch)
c.Check(cs.Revision, Equals, cand.Revision.N)
c.Check(cs.IgnoreValidation, Equals, cand.IgnoreValidation)
c.Check(t.logbuf.String(), Equals, "")
}
func (t *remoteRepoTestSuite) TestCurrentSnapIgnoreValidation(c *C) {
cand := &RefreshCandidate{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
Epoch: *snap.E("1"),
IgnoreValidation: true,
}
cs := currentSnap(cand)
c.Assert(cs, NotNil)
c.Check(cs.SnapID, Equals, cand.SnapID)
c.Check(cs.Channel, Equals, cand.Channel)
c.Check(cs.Epoch, DeepEquals, cand.Epoch)
c.Check(cs.Revision, Equals, cand.Revision.N)
c.Check(cs.IgnoreValidation, Equals, cand.IgnoreValidation)
c.Check(t.logbuf.String(), Equals, "")
}
func (t *remoteRepoTestSuite) TestCurrentSnapNoChannel(c *C) {
cand := &RefreshCandidate{
SnapID: helloWorldSnapID,
Revision: snap.R(1),
Epoch: *snap.E("1"),
}
cs := currentSnap(cand)
c.Assert(cs, NotNil)
c.Check(cs.SnapID, Equals, cand.SnapID)
c.Check(cs.Channel, Equals, "stable")
c.Check(cs.Epoch, DeepEquals, cand.Epoch)
c.Check(cs.Revision, Equals, cand.Revision.N)
c.Check(t.logbuf.String(), Equals, "")
}
func (t *remoteRepoTestSuite) TestCurrentSnapNilNoID(c *C) {
cand := &RefreshCandidate{
SnapID: "",
Revision: snap.R(1),
}
cs := currentSnap(cand)
c.Assert(cs, IsNil)
c.Check(t.logbuf.String(), Matches, "(?m).* an empty SnapID but a store revision!")
}
func (t *remoteRepoTestSuite) TestCurrentSnapNilLocalRevision(c *C) {
cand := &RefreshCandidate{
SnapID: helloWorldSnapID,
Revision: snap.R("x1"),
}
cs := currentSnap(cand)
c.Assert(cs, IsNil)
c.Check(t.logbuf.String(), Matches, "(?m).* a non-empty SnapID but a non-store revision!")
}
func (t *remoteRepoTestSuite) TestCurrentSnapNilLocalRevisionNoID(c *C) {
cand := &RefreshCandidate{
SnapID: "",
Revision: snap.R("x1"),
}
cs := currentSnap(cand)
c.Assert(cs, IsNil)
c.Check(t.logbuf.String(), Equals, "")
}
/* acquired via:
(against production "hello-world")
$ curl -s --data-binary '{"snaps":[{"snap_id":"buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ","channel":"stable","revision":25,"epoch":"0","confinement":"strict"}],"fields":["anon_download_url","architecture","channel","download_sha512","summary","description","binary_filesize","download_url","icon_url","last_updated","license","package_name","prices","publisher","ratings_average","revision","snap_id","support_url","title","content","version","origin","developer_id","private","confinement"]}' -H 'content-type: application/json' -H 'X-Ubuntu-Release: 16' -H 'X-Ubuntu-Wire-Protocol: 1' -H "accept: application/hal+json" https://api.snapcraft.io/api/v1/snaps/metadata | python3 -m json.tool --sort-keys | xsel -b
*/
var MockUpdatesJSON = `
{
"_embedded": {
"clickindex:package": [
{
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
"architecture": [
"all"
],
"binary_filesize": 20480,
"channel": "stable",
"confinement": "strict",
"content": "application",
"description": "This is a simple hello world example.",
"developer_id": "canonical",
"download_sha512": "345f33c06373f799b64c497a778ef58931810dd7ae85279d6917d8b4f43d38abaf37e68239cb85914db276cb566a0ef83ea02b6f2fd064b54f9f2508fa4ca1f1",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
"icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"last_updated": "2016-05-31T07:02:32.586839Z",
"license": "GPL-3.0",
"origin": "canonical",
"package_name": "hello-world",
"prices": {},
"publisher": "Canonical",
"ratings_average": 0.0,
"revision": 26,
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"summary": "Hello world example",
"support_url": "mailto:snappy-devel@lists.ubuntu.com",
"title": "Hello World",
"version": "6.1"
}
]
}
}
`
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRefreshForCandidates(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(1),
"epoch": "0",
"confinement": "",
})
c.Assert(resp.Fields, DeepEquals, detailFields)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.refreshForCandidates([]*currentSnapJSON{
{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
},
}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Name, Equals, "hello-world")
c.Assert(results[0].Revision, Equals, 26)
c.Assert(results[0].Version, Equals, "6.1")
c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
c.Assert(results[0].DeveloperID, Equals, helloWorldDeveloperID)
c.Assert(results[0].Deltas, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRefreshForCandidatesRetriesOnEOF(c *C) {
n := 0
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
n++
if n < 4 {
io.WriteString(w, "{")
mockServer.CloseClientConnections()
return
}
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err := json.NewDecoder(r.Body).Decode(&resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
}}, nil, nil)
c.Assert(err, IsNil)
c.Assert(n, Equals, 4)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Name, Equals, "hello-world")
}
func mockRFC(newRFC func(*Store, []*currentSnapJSON, *auth.UserState, *RefreshOptions) ([]*snapDetails, error)) func() {
oldRFC := refreshForCandidates
refreshForCandidates = newRFC
return func() {
refreshForCandidates = oldRFC
}
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryLookupRefresh(c *C) {
defer mockRFC(func(_ *Store, currentSnaps []*currentSnapJSON, _ *auth.UserState, _ *RefreshOptions) ([]*snapDetails, error) {
c.Check(currentSnaps, DeepEquals, []*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
Epoch: *snap.E("0"),
}})
return []*snapDetails{{
Name: "hello-world",
Revision: 26,
Version: "6.1",
SnapID: helloWorldSnapID,
DeveloperID: helloWorldDeveloperID,
}}, nil
})()
repo := New(nil, &testAuthContext{c: c, device: t.device})
c.Assert(repo, NotNil)
result, err := repo.LookupRefresh(&RefreshCandidate{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
Epoch: *snap.E("0"),
}, nil)
c.Assert(err, IsNil)
c.Assert(result.Name(), Equals, "hello-world")
c.Assert(result.Revision, Equals, snap.R(26))
c.Assert(result.Version, Equals, "6.1")
c.Assert(result.SnapID, Equals, helloWorldSnapID)
c.Assert(result.PublisherID, Equals, helloWorldDeveloperID)
c.Assert(result.Deltas, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryLookupRefreshIgnoreValidation(c *C) {
defer mockRFC(func(_ *Store, currentSnaps []*currentSnapJSON, _ *auth.UserState, _ *RefreshOptions) ([]*snapDetails, error) {
c.Check(currentSnaps, DeepEquals, []*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
Epoch: *snap.E("0"),
IgnoreValidation: true,
}})
return []*snapDetails{{
Name: "hello-world",
Revision: 26,
Version: "6.1",
SnapID: helloWorldSnapID,
DeveloperID: helloWorldDeveloperID,
}}, nil
})()
repo := New(nil, &testAuthContext{c: c, device: t.device})
c.Assert(repo, NotNil)
result, err := repo.LookupRefresh(&RefreshCandidate{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
Epoch: *snap.E("0"),
IgnoreValidation: true,
}, nil)
c.Assert(err, IsNil)
c.Assert(result.Name(), Equals, "hello-world")
c.Assert(result.Revision, Equals, snap.R(26))
c.Assert(result.SnapID, Equals, helloWorldSnapID)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryLookupRefreshLocalSnap(c *C) {
defer mockRFC(func(_ *Store, _ []*currentSnapJSON, _ *auth.UserState, _ *RefreshOptions) ([]*snapDetails, error) {
panic("unexpected call to refreshForCandidates")
})()
repo := New(nil, &testAuthContext{c: c, device: t.device})
c.Assert(repo, NotNil)
result, err := repo.LookupRefresh(&RefreshCandidate{
Revision: snap.R("x1"),
}, nil)
c.Assert(result, IsNil)
c.Check(err, Equals, ErrLocalSnap)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryLookupRefreshRFCError(c *C) {
anError := errors.New("ouchie")
defer mockRFC(func(_ *Store, _ []*currentSnapJSON, _ *auth.UserState, _ *RefreshOptions) ([]*snapDetails, error) {
return nil, anError
})()
repo := New(nil, &testAuthContext{c: c, device: t.device})
c.Assert(repo, NotNil)
result, err := repo.LookupRefresh(&RefreshCandidate{
SnapID: helloWorldDeveloperID,
Revision: snap.R(1),
}, nil)
c.Assert(result, IsNil)
c.Check(err, Equals, anError)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryLookupRefreshEmptyResponse(c *C) {
defer mockRFC(func(_ *Store, _ []*currentSnapJSON, _ *auth.UserState, _ *RefreshOptions) ([]*snapDetails, error) {
return nil, nil
})()
repo := New(nil, &testAuthContext{c: c, device: t.device})
c.Assert(repo, NotNil)
result, err := repo.LookupRefresh(&RefreshCandidate{
SnapID: helloWorldDeveloperID,
Revision: snap.R(1),
}, nil)
c.Assert(result, IsNil)
c.Check(err, Equals, ErrSnapNotFound)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryLookupRefreshNoUpdate(c *C) {
defer mockRFC(func(_ *Store, _ []*currentSnapJSON, _ *auth.UserState, _ *RefreshOptions) ([]*snapDetails, error) {
return []*snapDetails{{
SnapID: helloWorldDeveloperID,
Revision: 1,
}}, nil
})()
repo := New(nil, &testAuthContext{c: c, device: t.device})
c.Assert(repo, NotNil)
result, err := repo.LookupRefresh(&RefreshCandidate{
SnapID: helloWorldDeveloperID,
Revision: snap.R(1),
}, nil)
c.Assert(result, IsNil)
c.Check(err, Equals, ErrNoUpdateAvailable)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefresh(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(1),
"epoch": "0",
"confinement": "",
})
c.Assert(resp.Fields, DeepEquals, detailFields)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{
{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
},
}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Name(), Equals, "hello-world")
c.Assert(results[0].Revision, Equals, snap.R(26))
c.Assert(results[0].Version, Equals, "6.1")
c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID)
c.Assert(results[0].Deltas, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshIgnoreValidation(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(1),
"epoch": "0",
"confinement": "",
"ignore_validation": true,
})
c.Assert(resp.Fields, DeepEquals, detailFields)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{
{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
IgnoreValidation: true,
},
}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Name(), Equals, "hello-world")
c.Assert(results[0].Revision, Equals, snap.R(26))
c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshDefaultChannelIsStable(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(1),
"epoch": "0",
"confinement": "",
})
c.Assert(resp.Fields, DeepEquals, detailFields)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{
{
SnapID: helloWorldSnapID,
Revision: snap.R(1),
},
}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Name(), Equals, "hello-world")
c.Assert(results[0].Revision, Equals, snap.R(26))
c.Assert(results[0].Version, Equals, "6.1")
c.Assert(results[0].SnapID, Equals, helloWorldSnapID)
c.Assert(results[0].PublisherID, Equals, helloWorldDeveloperID)
c.Assert(results[0].Deltas, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshRetryOnEOF(c *C) {
n := 0
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
n++
if n < 4 {
io.WriteString(w, "{")
mockServer.CloseClientConnections()
return
}
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err := json.NewDecoder(r.Body).Decode(&resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(1),
}}, nil, nil)
c.Assert(err, IsNil)
c.Assert(n, Equals, 4)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Name(), Equals, "hello-world")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreUnexpectedEOFhandling(c *C) {
permanentlyBrokenSrvCalls := 0
somewhatBrokenSrvCalls := 0
mockPermanentlyBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
permanentlyBrokenSrvCalls++
w.Header().Add("Content-Length", "1000")
}))
mockSomewhatBrokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
somewhatBrokenSrvCalls++
if somewhatBrokenSrvCalls > 3 {
io.WriteString(w, MockUpdatesJSON)
return
}
w.Header().Add("Content-Length", "1000")
}))
queryServer := func(mockServer *httptest.Server) error {
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
_, err := repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
}}, nil, nil)
return err
}
// Check that we really recognize unexpected EOF error by failing on all retries
err := queryServer(mockPermanentlyBrokenServer)
c.Assert(err, NotNil)
c.Assert(err, Equals, io.ErrUnexpectedEOF)
c.Assert(err, ErrorMatches, "unexpected EOF")
// check that we exhausted all retries (as defined by mocked retry strategy)
c.Assert(permanentlyBrokenSrvCalls, Equals, 5)
// Check that we retry on unexpected EOF and eventually succeed
err = queryServer(mockSomewhatBrokenServer)
c.Assert(err, IsNil)
// check that we retried 4 times
c.Assert(somewhatBrokenSrvCalls, Equals, 4)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRefreshForCandidatesEOF(c *C) {
n := 0
var mockServer *httptest.Server
mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
n++
io.WriteString(w, "{")
mockServer.CloseClientConnections()
return
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
_, err := repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
}}, nil, nil)
c.Assert(err, NotNil)
c.Assert(err, ErrorMatches, `^Post http://127.0.0.1:.*?/metadata: EOF$`)
c.Assert(n, Equals, 5)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryRefreshForCandidatesUnauthorised(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
n++
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
w.WriteHeader(401)
io.WriteString(w, "")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
_, err := repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 24,
}}, nil, nil)
c.Assert(n, Equals, 1)
c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 401 via POST to "http://.*?/metadata"`)
}
func (t *remoteRepoTestSuite) TestRefreshForCandidatesFailOnDNS(c *C) {
baseURL, err := url.Parse("http://nonexistingserver909123.com/")
c.Assert(err, IsNil)
cfg := Config{
StoreBaseURL: baseURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
_, err = repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 24,
}}, nil, nil)
// the error differs depending on whether a proxy is in use (e.g. on travis), so don't inspect error message
c.Assert(err, NotNil)
}
func (t *remoteRepoTestSuite) TestRefreshForCandidates500(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
n++
w.WriteHeader(500)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
_, err := repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 24,
}}, nil, nil)
c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://.*?/metadata"`)
c.Assert(n, Equals, 5)
}
func (t *remoteRepoTestSuite) TestRefreshForCandidates500DurationExceeded(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
n++
time.Sleep(time.Duration(2) * time.Second)
w.WriteHeader(500)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
_, err := repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 24,
}}, nil, nil)
c.Assert(err, ErrorMatches, `cannot query the store for updates: got unexpected HTTP status code 500 via POST to "http://.*?/metadata"`)
c.Assert(n, Equals, 1)
}
func (t *remoteRepoTestSuite) TestAcceptableUpdateWorks(c *C) {
c.Check(acceptableUpdate(&snapDetails{Revision: 42}, &RefreshCandidate{Revision: snap.R("1")}), Equals, true)
}
func (t *remoteRepoTestSuite) TestAcceptableUpdateSkipsCurrent(c *C) {
c.Check(acceptableUpdate(&snapDetails{Revision: 42}, &RefreshCandidate{Revision: snap.R("42")}), Equals, false)
}
func (t *remoteRepoTestSuite) TestAcceptableUpdateSkipsBlocked(c *C) {
c.Check(acceptableUpdate(&snapDetails{Revision: 42}, &RefreshCandidate{Revision: snap.R("1"), Block: []snap.Revision{snap.R("42")}}), Equals, false)
}
func (t *remoteRepoTestSuite) TestAcceptableUpdateSkipsBoth(c *C) {
// belts-and-suspenders
c.Check(acceptableUpdate(&snapDetails{Revision: 42}, &RefreshCandidate{Revision: snap.R("42"), Block: []snap.Revision{snap.R("42")}}), Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipCurrent(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(26),
"epoch": "0",
"confinement": "",
})
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(26),
}}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshSkipBlocked(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(25),
"epoch": "0",
"confinement": "",
})
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(25),
Block: []snap.Revision{snap.R(26)},
}}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 0)
}
/* XXX Currently this is just MockUpdatesJSON with the deltas that we're
planning to add to the stores /api/v1/snaps/metadata response.
*/
var MockUpdatesWithDeltasJSON = `
{
"_embedded": {
"clickindex:package": [
{
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
"architecture": [
"all"
],
"binary_filesize": 20480,
"channel": "stable",
"confinement": "strict",
"content": "application",
"description": "This is a simple hello world example.",
"developer_id": "canonical",
"download_sha512": "345f33c06373f799b64c497a778ef58931810dd7ae85279d6917d8b4f43d38abaf37e68239cb85914db276cb566a0ef83ea02b6f2fd064b54f9f2508fa4ca1f1",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_26.snap",
"icon_url": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"last_updated": "2016-05-31T07:02:32.586839Z",
"license": "GPL-3.0",
"origin": "canonical",
"package_name": "hello-world",
"prices": {},
"publisher": "Canonical",
"ratings_average": 0.0,
"revision": 26,
"snap_id": "buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ",
"summary": "Hello world example",
"support_url": "mailto:snappy-devel@lists.ubuntu.com",
"title": "Hello World",
"version": "6.1",
"deltas": [{
"from_revision": 24,
"to_revision": 25,
"format": "xdelta3",
"binary_filesize": 204,
"download_sha3_384": "sha3_384_hash",
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta"
}, {
"from_revision": 25,
"to_revision": 26,
"format": "xdelta3",
"binary_filesize": 206,
"download_sha3_384": "sha3_384_hash",
"anon_download_url": "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta",
"download_url": "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta"
}]
}
]
}
}
`
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryDefaultsDeltasOnClassicOnly(c *C) {
for _, t := range []struct {
onClassic bool
deltaFormatStr string
}{
{false, ""},
{true, "xdelta3"},
} {
restore := release.MockOnClassic(t.onClassic)
defer restore()
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
c.Check(r.Header.Get("X-Ubuntu-Delta-Formats"), Equals, t.deltaFormatStr)
}))
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
repo.refreshForCandidates([]*currentSnapJSON{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: 1,
}}, nil, nil)
}
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithDeltas(c *C) {
origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
c.Check(r.Header.Get("X-Ubuntu-Delta-Formats"), Equals, `xdelta3`)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(24),
"epoch": "0",
"confinement": "",
})
c.Assert(resp.Fields, DeepEquals, getStructFields(snapDetails{}))
io.WriteString(w, MockUpdatesWithDeltasJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(24),
}}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Deltas, HasLen, 2)
c.Assert(results[0].Deltas[0], Equals, snap.DeltaInfo{
FromRevision: 24,
ToRevision: 25,
Format: "xdelta3",
AnonDownloadURL: "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta",
DownloadURL: "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_24_25_xdelta3.delta",
Size: 204,
Sha3_384: "sha3_384_hash",
})
c.Assert(results[0].Deltas[1], Equals, snap.DeltaInfo{
FromRevision: 25,
ToRevision: 26,
Format: "xdelta3",
AnonDownloadURL: "https://public.apps.ubuntu.com/anon/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta",
DownloadURL: "https://public.apps.ubuntu.com/download-snap/buPKUD3TKqCOgLEjjHx5kSiCpIs5cMuQ_25_26_xdelta3.delta",
Size: 206,
Sha3_384: "sha3_384_hash",
})
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshWithoutDeltas(c *C) {
// Verify the X-Delta-Format header is not set.
origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "0"), IsNil)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
c.Check(r.Header.Get("X-Ubuntu-Delta-Formats"), Equals, ``)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
var resp struct {
Snaps []map[string]interface{} `json:"snaps"`
Fields []string `json:"fields"`
}
err = json.Unmarshal(jsonReq, &resp)
c.Assert(err, IsNil)
c.Assert(resp.Snaps, HasLen, 1)
c.Assert(resp.Snaps[0], DeepEquals, map[string]interface{}{
"snap_id": helloWorldSnapID,
"channel": "stable",
"revision": float64(24),
"epoch": "0",
"confinement": "",
})
c.Assert(resp.Fields, DeepEquals, detailFields)
io.WriteString(w, MockUpdatesJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
results, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(24),
}}, nil, nil)
c.Assert(err, IsNil)
c.Assert(results, HasLen, 1)
c.Assert(results[0].Deltas, HasLen, 0)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryUpdateNotSendLocalRevs(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Error(r.URL.Path)
c.Fatal("no network request expected")
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
_, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(-2),
}}, nil, nil)
c.Assert(err, IsNil)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryListRefreshOptions(c *C) {
for _, t := range []struct {
flag *RefreshOptions
header string
}{
{nil, ""},
{&RefreshOptions{RefreshManaged: true}, "X-Ubuntu-Refresh-Managed"},
} {
mockServerHit := false
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "POST", metadataPath)
if t.header != "" {
c.Check(r.Header.Get(t.header), Equals, "true")
}
mockServerHit = true
io.WriteString(w, `{}`)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
_, err := repo.ListRefresh([]*RefreshCandidate{{
SnapID: helloWorldSnapID,
Channel: "stable",
Revision: snap.R(24),
}}, nil, t.flag)
c.Assert(err, IsNil)
c.Check(mockServerHit, Equals, true)
}
}
func (t *remoteRepoTestSuite) TestStructFieldsSurvivesNoTag(c *C) {
type s struct {
Foo int `json:"hello"`
Bar int
}
c.Assert(getStructFields(s{}), DeepEquals, []string{"hello"})
}
func (t *remoteRepoTestSuite) TestAuthLocationDependsOnEnviron(c *C) {
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
before := authLocation()
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
after := authLocation()
c.Check(before, Not(Equals), after)
}
func (t *remoteRepoTestSuite) TestAuthURLDependsOnEnviron(c *C) {
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
before := authURL()
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
after := authURL()
c.Check(before, Not(Equals), after)
}
func (t *remoteRepoTestSuite) TestApiURLDependsOnEnviron(c *C) {
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
before := apiURL()
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
after := apiURL()
c.Check(before, Not(Equals), after)
}
func (t *remoteRepoTestSuite) TestStoreURLDependsOnEnviron(c *C) {
// This also depends on the API URL, but that's tested separately (see
// TestApiURLDependsOnEnviron).
api := apiURL()
c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", ""), IsNil)
c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", ""), IsNil)
// Test in order of precedence (low first) leaving env vars set as we go ...
u, err := storeURL(api)
c.Assert(err, IsNil)
c.Check(u.String(), Matches, api.String()+".*")
c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "https://force-api.local/"), IsNil)
defer os.Setenv("SNAPPY_FORCE_API_URL", "")
u, err = storeURL(api)
c.Assert(err, IsNil)
c.Check(u.String(), Matches, "https://force-api.local/.*")
c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", "https://force-cpi.local/api/v1/"), IsNil)
defer os.Setenv("SNAPPY_FORCE_CPI_URL", "")
u, err = storeURL(api)
c.Assert(err, IsNil)
c.Check(u.String(), Matches, "https://force-cpi.local/.*")
}
func (t *remoteRepoTestSuite) TestStoreURLBadEnvironAPI(c *C) {
c.Assert(os.Setenv("SNAPPY_FORCE_API_URL", "://force-api.local/"), IsNil)
defer os.Setenv("SNAPPY_FORCE_API_URL", "")
_, err := storeURL(apiURL())
c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_API_URL: parse ://force-api.local/: missing protocol scheme")
}
func (t *remoteRepoTestSuite) TestStoreURLBadEnvironCPI(c *C) {
c.Assert(os.Setenv("SNAPPY_FORCE_CPI_URL", "://force-cpi.local/api/v1/"), IsNil)
defer os.Setenv("SNAPPY_FORCE_CPI_URL", "")
_, err := storeURL(apiURL())
c.Check(err, ErrorMatches, "invalid SNAPPY_FORCE_CPI_URL: parse ://force-cpi.local/: missing protocol scheme")
}
func (t *remoteRepoTestSuite) TestMyAppsURLDependsOnEnviron(c *C) {
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", ""), IsNil)
before := myappsURL()
c.Assert(os.Setenv("SNAPPY_USE_STAGING_STORE", "1"), IsNil)
defer os.Setenv("SNAPPY_USE_STAGING_STORE", "")
after := myappsURL()
c.Check(before, Not(Equals), after)
}
func (t *remoteRepoTestSuite) TestDefaultConfig(c *C) {
c.Check(defaultConfig.StoreBaseURL.String(), Equals, "https://api.snapcraft.io/")
c.Check(defaultConfig.AssertionsBaseURL, IsNil)
}
func (t *remoteRepoTestSuite) TestNew(c *C) {
aStore := New(nil, nil)
// check for fields
c.Check(aStore.detailFields, DeepEquals, detailFields)
}
var testAssertion = `type: snap-declaration
authority-id: super
series: 16
snap-id: snapidfoo
publisher-id: devidbaz
snap-name: mysnap
timestamp: 2016-03-30T12:22:16Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
openpgp wsBcBAABCAAQBQJW+8VBCRDWhXkqAWcrfgAAQ9gIABZFgMPByJZeUE835FkX3/y2hORn
AzE3R1ktDkQEVe/nfVDMACAuaw1fKmUS4zQ7LIrx/AZYw5i0vKVmJszL42LBWVsqR0+p9Cxebzv9
U2VUSIajEsUUKkBwzD8wxFzagepFlScif1NvCGZx0vcGUOu0Ent0v+gqgAv21of4efKqEW7crlI1
T/A8LqZYmIzKRHGwCVucCyAUD8xnwt9nyWLgLB+LLPOVFNK8SR6YyNsX05Yz1BUSndBfaTN8j/k8
8isKGZE6P0O9ozBbNIAE8v8NMWQegJ4uWuil7D3psLkzQIrxSypk9TrQ2GlIG2hJdUovc5zBuroe
xS4u9rVT6UY=`
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion(c *C) {
restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 88)
defer restore()
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", "/api/v1/snaps/assertions/.*")
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
c.Check(r.URL.Path, Matches, ".*/snap-declaration/16/snapidfoo")
c.Check(r.URL.Query().Get("max-format"), Equals, "88")
io.WriteString(w, testAssertion)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
authContext := &testAuthContext{c: c, device: t.device}
repo := New(&cfg, authContext)
a, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
c.Assert(err, IsNil)
c.Check(a, NotNil)
c.Check(a.Type(), Equals, asserts.SnapDeclarationType)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionProxyStoreFromAuthContext(c *C) {
restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 88)
defer restore()
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", "/api/v1/snaps/assertions/.*")
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
c.Check(r.URL.Path, Matches, ".*/snap-declaration/16/snapidfoo")
c.Check(r.URL.Query().Get("max-format"), Equals, "88")
io.WriteString(w, testAssertion)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
nowhereURL, err := url.Parse("http://nowhere.invalid")
c.Assert(err, IsNil)
cfg := Config{
AssertionsBaseURL: nowhereURL,
}
authContext := &testAuthContext{
c: c,
device: t.device,
proxyStoreID: "foo",
proxyStoreURL: mockServerURL,
}
repo := New(&cfg, authContext)
a, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
c.Assert(err, IsNil)
c.Check(a, NotNil)
c.Check(a.Type(), Equals, asserts.SnapDeclarationType)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertionNotFound(c *C) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", "/api/v1/snaps/assertions/.*")
c.Check(r.Header.Get("Accept"), Equals, "application/x.ubuntu.assertion")
c.Check(r.URL.Path, Matches, ".*/snap-declaration/16/snapidfoo")
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(404)
io.WriteString(w, `{"status": 404,"title": "not found"}`)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
AssertionsBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
_, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
c.Check(asserts.IsNotFound(err), Equals, true)
c.Check(err, DeepEquals, &asserts.NotFoundError{
Type: asserts.SnapDeclarationType,
Headers: map[string]string{
"series": "16",
"snap-id": "snapidfoo",
},
})
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositoryAssertion500(c *C) {
var n = 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", "/api/v1/snaps/assertions/.*")
n++
w.WriteHeader(500)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
AssertionsBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
_, err := repo.Assertion(asserts.SnapDeclarationType, []string{"16", "snapidfoo"}, nil)
c.Assert(err, ErrorMatches, `cannot fetch assertion: got unexpected HTTP status code 500 via .+`)
c.Assert(n, Equals, 5)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreRepositorySuggestedCurrency(c *C) {
suggestedCurrency := "GBP"
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", detailsPathPattern)
w.Header().Set("X-Suggested-Currency", suggestedCurrency)
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
// the store doesn't know the currency until after the first search, so fall back to dollars
c.Check(repo.SuggestedCurrency(), Equals, "USD")
// we should soon have a suggested currency
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
result, err := repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Assert(result, NotNil)
c.Check(repo.SuggestedCurrency(), Equals, "GBP")
suggestedCurrency = "EUR"
// checking the currency updates
result, err = repo.SnapInfo(spec, nil)
c.Assert(err, IsNil)
c.Assert(result, NotNil)
c.Check(repo.SuggestedCurrency(), Equals, "EUR")
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrders(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", ordersPath)
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.URL.Path, Equals, ordersPath)
io.WriteString(w, mockOrdersJSON)
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
helloWorld.Prices = map[string]float64{"USD": 1.23}
helloWorld.Paid = true
funkyApp := &snap.Info{}
funkyApp.SnapID = funkyAppSnapID
funkyApp.Prices = map[string]float64{"USD": 2.34}
funkyApp.Paid = true
otherApp := &snap.Info{}
otherApp.SnapID = "other"
otherApp.Prices = map[string]float64{"USD": 3.45}
otherApp.Paid = true
otherApp2 := &snap.Info{}
otherApp2.SnapID = "other2"
snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, IsNil)
c.Check(helloWorld.MustBuy, Equals, false)
c.Check(funkyApp.MustBuy, Equals, false)
c.Check(otherApp.MustBuy, Equals, true)
c.Check(otherApp2.MustBuy, Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersFailedAccess(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", ordersPath)
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, ordersPath)
w.WriteHeader(401)
io.WriteString(w, "{}")
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
helloWorld.Prices = map[string]float64{"USD": 1.23}
helloWorld.Paid = true
funkyApp := &snap.Info{}
funkyApp.SnapID = funkyAppSnapID
funkyApp.Prices = map[string]float64{"USD": 2.34}
funkyApp.Paid = true
otherApp := &snap.Info{}
otherApp.SnapID = "other"
otherApp.Prices = map[string]float64{"USD": 3.45}
otherApp.Paid = true
otherApp2 := &snap.Info{}
otherApp2.SnapID = "other2"
snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, NotNil)
c.Check(helloWorld.MustBuy, Equals, true)
c.Check(funkyApp.MustBuy, Equals, true)
c.Check(otherApp.MustBuy, Equals, true)
c.Check(otherApp2.MustBuy, Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersNoAuth(c *C) {
cfg := Config{}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
helloWorld.Prices = map[string]float64{"USD": 1.23}
helloWorld.Paid = true
funkyApp := &snap.Info{}
funkyApp.SnapID = funkyAppSnapID
funkyApp.Prices = map[string]float64{"USD": 2.34}
funkyApp.Paid = true
otherApp := &snap.Info{}
otherApp.SnapID = "other"
otherApp.Prices = map[string]float64{"USD": 3.45}
otherApp.Paid = true
otherApp2 := &snap.Info{}
otherApp2.SnapID = "other2"
snaps := []*snap.Info{helloWorld, funkyApp, otherApp, otherApp2}
err := repo.decorateOrders(snaps, nil)
c.Assert(err, IsNil)
c.Check(helloWorld.MustBuy, Equals, true)
c.Check(funkyApp.MustBuy, Equals, true)
c.Check(otherApp.MustBuy, Equals, true)
c.Check(otherApp2.MustBuy, Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersAllFree(c *C) {
requestRecieved := false
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Error(r.URL.Path)
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
requestRecieved = true
io.WriteString(w, `{"orders": []}`)
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
// This snap is free
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
// This snap is also free
funkyApp := &snap.Info{}
funkyApp.SnapID = funkyAppSnapID
snaps := []*snap.Info{helloWorld, funkyApp}
// There should be no request to the purchase server.
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, IsNil)
c.Check(requestRecieved, Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingle(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, ordersPath)
io.WriteString(w, mockSingleOrderJSON)
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
helloWorld.Prices = map[string]float64{"USD": 1.23}
helloWorld.Paid = true
snaps := []*snap.Info{helloWorld}
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, IsNil)
c.Check(helloWorld.MustBuy, Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingleFreeSnap(c *C) {
cfg := Config{}
repo := New(&cfg, nil)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
snaps := []*snap.Info{helloWorld}
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, IsNil)
c.Check(helloWorld.MustBuy, Equals, false)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersSingleNotFound(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", ordersPath)
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, ordersPath)
w.WriteHeader(404)
io.WriteString(w, "{}")
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
helloWorld.Prices = map[string]float64{"USD": 1.23}
helloWorld.Paid = true
snaps := []*snap.Info{helloWorld}
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, NotNil)
c.Check(helloWorld.MustBuy, Equals, true)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreDecorateOrdersTokenExpired(c *C) {
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, ordersPath)
w.WriteHeader(401)
io.WriteString(w, "")
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
helloWorld := &snap.Info{}
helloWorld.SnapID = helloWorldSnapID
helloWorld.Prices = map[string]float64{"USD": 1.23}
helloWorld.Paid = true
snaps := []*snap.Info{helloWorld}
err := repo.decorateOrders(snaps, t.user)
c.Assert(err, NotNil)
c.Check(helloWorld.MustBuy, Equals, true)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreMustBuy(c *C) {
// Never need to buy a free snap.
c.Check(mustBuy(false, true), Equals, false)
c.Check(mustBuy(false, false), Equals, false)
// Don't need to buy snaps that have been bought.
c.Check(mustBuy(true, true), Equals, false)
// Need to buy snaps that aren't bought.
c.Check(mustBuy(true, false), Equals, true)
}
const customersMeValid = `
{
"latest_tos_date": "2016-09-14T00:00:00+00:00",
"accepted_tos_date": "2016-09-14T15:56:49+00:00",
"latest_tos_accepted": true,
"has_payment_method": true
}
`
var buyTests = []struct {
suggestedCurrency string
expectedInput string
buyStatus int
buyResponse string
buyErrorMessage string
buyErrorCode string
snapID string
price float64
currency string
expectedResult *BuyResult
expectedError string
}{
{
// successful buying
suggestedCurrency: "EUR",
expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"0.99","currency":"EUR"}`,
buyResponse: mockOrderResponseJSON,
expectedResult: &BuyResult{State: "Complete"},
},
{
// failure due to invalid price
suggestedCurrency: "USD",
expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"5.99","currency":"USD"}`,
buyStatus: 400,
buyErrorCode: "invalid-field",
buyErrorMessage: "invalid price specified",
price: 5.99,
expectedError: "cannot buy snap: bad request: invalid price specified",
},
{
// failure due to unknown snap ID
suggestedCurrency: "USD",
expectedInput: `{"snap_id":"invalid snap ID","amount":"0.99","currency":"EUR"}`,
buyStatus: 404,
buyErrorCode: "not-found",
buyErrorMessage: "Snap package not found",
snapID: "invalid snap ID",
price: 0.99,
currency: "EUR",
expectedError: "cannot buy snap: server says not found: Snap package not found",
},
{
// failure due to "Purchase failed"
suggestedCurrency: "USD",
expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`,
buyStatus: 402, // Payment Required
buyErrorCode: "request-failed",
buyErrorMessage: "Purchase failed",
expectedError: "payment declined",
},
{
// failure due to no payment methods
suggestedCurrency: "USD",
expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`,
buyStatus: 403,
buyErrorCode: "no-payment-methods",
buyErrorMessage: "No payment methods associated with your account.",
expectedError: "no payment methods",
},
{
// failure due to terms of service not accepted
suggestedCurrency: "USD",
expectedInput: `{"snap_id":"` + helloWorldSnapID + `","amount":"1.23","currency":"USD"}`,
buyStatus: 403,
buyErrorCode: "tos-not-accepted",
buyErrorMessage: "You must accept the latest terms of service first.",
expectedError: "terms of service not accepted",
},
}
func (t *remoteRepoTestSuite) TestUbuntuStoreBuy500(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case detailsPath("hello-world"):
n++
w.WriteHeader(500)
case buyPath:
case customersMePath:
// default 200 response
default:
c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
buyOptions := &BuyOptions{
SnapID: helloWorldSnapID,
Currency: "USD",
Price: 1,
}
_, err := repo.Buy(buyOptions, t.user)
c.Assert(err, NotNil)
}
func (t *remoteRepoTestSuite) TestUbuntuStoreBuy(c *C) {
for _, test := range buyTests {
searchServerCalled := false
purchaseServerGetCalled := false
purchaseServerPostCalled := false
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case detailsPath("hello-world"):
c.Assert(r.Method, Equals, "GET")
w.Header().Set("Content-Type", "application/hal+json")
w.Header().Set("X-Suggested-Currency", test.suggestedCurrency)
w.WriteHeader(200)
io.WriteString(w, MockDetailsJSON)
searchServerCalled = true
case ordersPath:
c.Assert(r.Method, Equals, "GET")
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
io.WriteString(w, `{"orders": []}`)
purchaseServerGetCalled = true
case buyPath:
c.Assert(r.Method, Equals, "POST")
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.Header.Get("Content-Type"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, buyPath)
jsonReq, err := ioutil.ReadAll(r.Body)
c.Assert(err, IsNil)
c.Check(string(jsonReq), Equals, test.expectedInput)
if test.buyErrorCode == "" {
io.WriteString(w, test.buyResponse)
} else {
w.WriteHeader(test.buyStatus)
// TODO(matt): this is fugly!
fmt.Fprintf(w, `
{
"error_list": [
{
"code": "%s",
"message": "%s"
}
]
}`, test.buyErrorCode, test.buyErrorMessage)
}
purchaseServerPostCalled = true
default:
c.Fatalf("unexpected query %s %s", r.Method, r.URL.Path)
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
mockServerURL, _ := url.Parse(mockServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
// Find the snap first
spec := SnapSpec{
Name: "hello-world",
Channel: "edge",
Revision: snap.R(0),
}
snap, err := repo.SnapInfo(spec, t.user)
c.Assert(snap, NotNil)
c.Assert(err, IsNil)
buyOptions := &BuyOptions{
SnapID: snap.SnapID,
Currency: repo.SuggestedCurrency(),
Price: snap.Prices[repo.SuggestedCurrency()],
}
if test.snapID != "" {
buyOptions.SnapID = test.snapID
}
if test.currency != "" {
buyOptions.Currency = test.currency
}
if test.price > 0 {
buyOptions.Price = test.price
}
result, err := repo.Buy(buyOptions, t.user)
c.Check(result, DeepEquals, test.expectedResult)
if test.expectedError == "" {
c.Check(err, IsNil)
} else {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, test.expectedError)
}
c.Check(searchServerCalled, Equals, true)
c.Check(purchaseServerGetCalled, Equals, true)
c.Check(purchaseServerPostCalled, Equals, true)
}
}
func (t *remoteRepoTestSuite) TestUbuntuStoreBuyFailArgumentChecking(c *C) {
repo := New(&Config{}, nil)
c.Assert(repo, NotNil)
// no snap ID
result, err := repo.Buy(&BuyOptions{
Price: 1.0,
Currency: "USD",
}, t.user)
c.Assert(result, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "cannot buy snap: snap ID missing")
// no price
result, err = repo.Buy(&BuyOptions{
SnapID: "snap ID",
Currency: "USD",
}, t.user)
c.Assert(result, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "cannot buy snap: invalid expected price")
// no currency
result, err = repo.Buy(&BuyOptions{
SnapID: "snap ID",
Price: 1.0,
}, t.user)
c.Assert(result, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "cannot buy snap: currency missing")
// no user
result, err = repo.Buy(&BuyOptions{
SnapID: "snap ID",
Price: 1.0,
Currency: "USD",
}, nil)
c.Assert(result, IsNil)
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "you need to log in first")
}
var readyToBuyTests = []struct {
Input func(w http.ResponseWriter)
Test func(c *C, err error)
NumOfCalls int
}{
{
// A user account the is ready for buying
Input: func(w http.ResponseWriter) {
io.WriteString(w, `
{
"latest_tos_date": "2016-09-14T00:00:00+00:00",
"accepted_tos_date": "2016-09-14T15:56:49+00:00",
"latest_tos_accepted": true,
"has_payment_method": true
}
`)
},
Test: func(c *C, err error) {
c.Check(err, IsNil)
},
NumOfCalls: 1,
},
{
// A user account that hasn't accepted the TOS
Input: func(w http.ResponseWriter) {
io.WriteString(w, `
{
"latest_tos_date": "2016-10-14T00:00:00+00:00",
"accepted_tos_date": "2016-09-14T15:56:49+00:00",
"latest_tos_accepted": false,
"has_payment_method": true
}
`)
},
Test: func(c *C, err error) {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "terms of service not accepted")
},
NumOfCalls: 1,
},
{
// A user account that has no payment method
Input: func(w http.ResponseWriter) {
io.WriteString(w, `
{
"latest_tos_date": "2016-10-14T00:00:00+00:00",
"accepted_tos_date": "2016-09-14T15:56:49+00:00",
"latest_tos_accepted": true,
"has_payment_method": false
}
`)
},
Test: func(c *C, err error) {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "no payment methods")
},
NumOfCalls: 1,
},
{
// A user account that has no payment method and has not accepted the TOS
Input: func(w http.ResponseWriter) {
io.WriteString(w, `
{
"latest_tos_date": "2016-10-14T00:00:00+00:00",
"accepted_tos_date": "2016-09-14T15:56:49+00:00",
"latest_tos_accepted": false,
"has_payment_method": false
}
`)
},
Test: func(c *C, err error) {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "no payment methods")
},
NumOfCalls: 1,
},
{
// No user account exists
Input: func(w http.ResponseWriter) {
w.WriteHeader(404)
io.WriteString(w, "{}")
},
Test: func(c *C, err error) {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, "cannot get customer details: server says no account exists")
},
NumOfCalls: 1,
},
{
// An unknown set of errors occurs
Input: func(w http.ResponseWriter) {
w.WriteHeader(500)
io.WriteString(w, `
{
"error_list": [
{
"code": "code 1",
"message": "message 1"
},
{
"code": "code 2",
"message": "message 2"
}
]
}`)
},
Test: func(c *C, err error) {
c.Assert(err, NotNil)
c.Check(err.Error(), Equals, `message 1`)
},
NumOfCalls: 5,
},
}
func (t *remoteRepoTestSuite) TestUbuntuStoreReadyToBuy(c *C) {
for _, test := range readyToBuyTests {
purchaseServerGetCalled := 0
mockPurchasesServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(c, r, "GET", customersMePath)
switch r.Method {
case "GET":
// check device authorization is set, implicitly checking doRequest was used
c.Check(r.Header.Get("X-Device-Authorization"), Equals, `Macaroon root="device-macaroon"`)
c.Check(r.Header.Get("Authorization"), Equals, t.expectedAuthorization(c, t.user))
c.Check(r.Header.Get("Accept"), Equals, jsonContentType)
c.Check(r.URL.Path, Equals, customersMePath)
test.Input(w)
purchaseServerGetCalled++
default:
c.Error("Unexpected request method: ", r.Method)
}
}))
c.Assert(mockPurchasesServer, NotNil)
defer mockPurchasesServer.Close()
mockServerURL, _ := url.Parse(mockPurchasesServer.URL)
authContext := &testAuthContext{c: c, device: t.device, user: t.user}
cfg := Config{
StoreBaseURL: mockServerURL,
}
repo := New(&cfg, authContext)
c.Assert(repo, NotNil)
err := repo.ReadyToBuy(t.user)
test.Test(c, err)
c.Check(purchaseServerGetCalled, Equals, test.NumOfCalls)
}
}
func (t *remoteRepoTestSuite) TestDoRequestSetRangeHeaderOnRedirect(c *C) {
n := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch n {
case 0:
http.Redirect(w, r, r.URL.Path+"-else", 302)
n++
case 1:
c.Check(r.URL.Path, Equals, "/somewhere-else")
rg := r.Header.Get("Range")
c.Check(rg, Equals, "bytes=5-")
default:
panic("got more than 2 requests in this test")
}
}))
c.Assert(mockServer, NotNil)
defer mockServer.Close()
url, err := url.Parse(mockServer.URL + "/somewhere")
c.Assert(err, IsNil)
reqOptions := &requestOptions{
Method: "GET",
URL: url,
ExtraHeaders: map[string]string{
"Range": "bytes=5-",
},
}
sto := New(&Config{}, nil)
_, err = sto.doRequest(context.TODO(), sto.client, reqOptions, t.user)
c.Assert(err, IsNil)
}
type cacheObserver struct {
inCache map[string]bool
gets []string
puts []string
}
func (co *cacheObserver) Get(cacheKey, targetPath string) error {
co.gets = append(co.gets, fmt.Sprintf("%s:%s", cacheKey, targetPath))
if !co.inCache[cacheKey] {
return fmt.Errorf("cannot find %s in cache", cacheKey)
}
return nil
}
func (co *cacheObserver) Put(cacheKey, sourcePath string) error {
co.puts = append(co.puts, fmt.Sprintf("%s:%s", cacheKey, sourcePath))
return nil
}
func (t *remoteRepoTestSuite) TestDownloadCacheHit(c *C) {
oldCache := t.store.cacher
defer func() { t.store.cacher = oldCache }()
obs := &cacheObserver{inCache: map[string]bool{"the-snaps-sha3_384": true}}
t.store.cacher = obs
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
c.Fatalf("download should not be called when results come from the cache")
return nil
}
snap := &snap.Info{}
snap.Sha3_384 = "the-snaps-sha3_384"
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("%s:%s", snap.Sha3_384, path)})
c.Check(obs.puts, IsNil)
}
func (t *remoteRepoTestSuite) TestDownloadCacheMiss(c *C) {
oldCache := t.store.cacher
defer func() { t.store.cacher = oldCache }()
obs := &cacheObserver{inCache: map[string]bool{}}
t.store.cacher = obs
downloadWasCalled := false
download = func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter) error {
downloadWasCalled = true
return nil
}
snap := &snap.Info{}
snap.Sha3_384 = "the-snaps-sha3_384"
path := filepath.Join(c.MkDir(), "downloaded-file")
err := t.store.Download(context.TODO(), "foo", path, &snap.DownloadInfo, nil, nil)
c.Assert(err, IsNil)
c.Check(downloadWasCalled, Equals, true)
c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)})
c.Check(obs.puts, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)})
}