github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/store/download_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2018 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package store_test
    21  
    22  import (
    23  	"bytes"
    24  	"context"
    25  	"crypto"
    26  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"io/ioutil"
    30  	"net/http"
    31  	"net/http/httptest"
    32  	"os"
    33  	"path/filepath"
    34  	"time"
    35  
    36  	"github.com/juju/ratelimit"
    37  	. "gopkg.in/check.v1"
    38  	"gopkg.in/retry.v1"
    39  
    40  	"github.com/snapcore/snapd/dirs"
    41  	"github.com/snapcore/snapd/osutil"
    42  	"github.com/snapcore/snapd/overlord/auth"
    43  	"github.com/snapcore/snapd/progress"
    44  	"github.com/snapcore/snapd/release"
    45  	"github.com/snapcore/snapd/snap"
    46  	"github.com/snapcore/snapd/store"
    47  	"github.com/snapcore/snapd/testutil"
    48  )
    49  
    50  type downloadSuite struct {
    51  	testutil.BaseTest
    52  }
    53  
    54  var _ = Suite(&downloadSuite{})
    55  
    56  func (s *downloadSuite) SetUpTest(c *C) {
    57  	s.BaseTest.SetUpTest(c)
    58  
    59  	store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.Exponential{
    60  		Initial: time.Millisecond,
    61  		Factor:  2.5,
    62  	}))
    63  
    64  	mockXdelta := testutil.MockCommand(c, "xdelta3", "")
    65  	s.AddCleanup(mockXdelta.Restore)
    66  }
    67  
    68  func (s *downloadSuite) TestActualDownload(c *C) {
    69  	n := 0
    70  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    71  		c.Check(r.Header.Get("Snap-CDN"), Equals, "")
    72  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "")
    73  		n++
    74  		io.WriteString(w, "response-data")
    75  	}))
    76  	c.Assert(mockServer, NotNil)
    77  	defer mockServer.Close()
    78  
    79  	theStore := store.New(&store.Config{}, nil)
    80  	var buf SillyBuffer
    81  	// keep tests happy
    82  	sha3 := ""
    83  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
    84  	c.Assert(err, IsNil)
    85  	c.Check(buf.String(), Equals, "response-data")
    86  	c.Check(n, Equals, 1)
    87  }
    88  
    89  func (s *downloadSuite) TestActualDownloadAutoRefresh(c *C) {
    90  	n := 0
    91  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    92  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled")
    93  		n++
    94  		io.WriteString(w, "response-data")
    95  	}))
    96  	c.Assert(mockServer, NotNil)
    97  	defer mockServer.Close()
    98  
    99  	theStore := store.New(&store.Config{}, nil)
   100  	var buf SillyBuffer
   101  	// keep tests happy
   102  	sha3 := ""
   103  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{IsAutoRefresh: true})
   104  	c.Assert(err, IsNil)
   105  	c.Check(buf.String(), Equals, "response-data")
   106  	c.Check(n, Equals, 1)
   107  }
   108  
   109  func (s *downloadSuite) TestActualDownloadNoCDN(c *C) {
   110  	os.Setenv("SNAPPY_STORE_NO_CDN", "1")
   111  	defer os.Unsetenv("SNAPPY_STORE_NO_CDN")
   112  
   113  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   114  		c.Check(r.Header.Get("Snap-CDN"), Equals, "none")
   115  		io.WriteString(w, "response-data")
   116  	}))
   117  	c.Assert(mockServer, NotNil)
   118  	defer mockServer.Close()
   119  
   120  	theStore := store.New(&store.Config{}, nil)
   121  	var buf SillyBuffer
   122  	// keep tests happy
   123  	sha3 := ""
   124  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   125  	c.Assert(err, IsNil)
   126  	c.Check(buf.String(), Equals, "response-data")
   127  }
   128  
   129  func (s *downloadSuite) TestActualDownloadFullCloudInfoFromAuthContext(c *C) {
   130  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   131  		c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`)
   132  
   133  		io.WriteString(w, "response-data")
   134  	}))
   135  	c.Assert(mockServer, NotNil)
   136  	defer mockServer.Close()
   137  
   138  	device := createTestDevice()
   139  	theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "aws", Region: "us-east-1", AvailabilityZone: "us-east-1c"}})
   140  
   141  	var buf SillyBuffer
   142  	// keep tests happy
   143  	sha3 := ""
   144  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   145  	c.Assert(err, IsNil)
   146  	c.Check(buf.String(), Equals, "response-data")
   147  }
   148  
   149  func (s *downloadSuite) TestActualDownloadLessDetailedCloudInfoFromAuthContext(c *C) {
   150  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   151  		c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="openstack" availability-zone="nova"`)
   152  
   153  		io.WriteString(w, "response-data")
   154  	}))
   155  	c.Assert(mockServer, NotNil)
   156  	defer mockServer.Close()
   157  
   158  	device := createTestDevice()
   159  	theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "openstack", Region: "", AvailabilityZone: "nova"}})
   160  
   161  	var buf SillyBuffer
   162  	// keep tests happy
   163  	sha3 := ""
   164  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   165  	c.Assert(err, IsNil)
   166  	c.Check(buf.String(), Equals, "response-data")
   167  }
   168  
   169  func (s *downloadSuite) TestDownloadCancellation(c *C) {
   170  	// the channel used by mock server to request cancellation from the test
   171  	syncCh := make(chan struct{})
   172  
   173  	n := 0
   174  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   175  		n++
   176  		io.WriteString(w, "foo")
   177  		syncCh <- struct{}{}
   178  		io.WriteString(w, "bar")
   179  		time.Sleep(10 * time.Millisecond)
   180  	}))
   181  	c.Assert(mockServer, NotNil)
   182  	defer mockServer.Close()
   183  
   184  	theStore := store.New(&store.Config{}, nil)
   185  
   186  	ctx, cancel := context.WithCancel(context.Background())
   187  
   188  	result := make(chan string)
   189  	go func() {
   190  		sha3 := ""
   191  		var buf SillyBuffer
   192  		err := store.Download(ctx, "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   193  		result <- err.Error()
   194  		close(result)
   195  	}()
   196  
   197  	<-syncCh
   198  	cancel()
   199  
   200  	err := <-result
   201  	c.Check(n, Equals, 1)
   202  	c.Assert(err, Equals, "The download has been cancelled: context canceled")
   203  }
   204  
   205  type nopeSeeker struct{ io.ReadWriter }
   206  
   207  func (nopeSeeker) Seek(int64, int) (int64, error) {
   208  	return -1, errors.New("what is this, quidditch?")
   209  }
   210  
   211  func (s *downloadSuite) TestActualDownloadNonPurchased402(c *C) {
   212  	n := 0
   213  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   214  		n++
   215  		// XXX: the server doesn't behave correctly ATM
   216  		// but 401 for paid snaps is the unlikely case so far
   217  		w.WriteHeader(402)
   218  	}))
   219  	c.Assert(mockServer, NotNil)
   220  	defer mockServer.Close()
   221  
   222  	theStore := store.New(&store.Config{}, nil)
   223  	var buf bytes.Buffer
   224  	err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, nopeSeeker{&buf}, -1, nil, nil)
   225  	c.Assert(err, NotNil)
   226  	c.Check(err.Error(), Equals, "please buy foo before installing it.")
   227  	c.Check(n, Equals, 1)
   228  }
   229  
   230  func (s *downloadSuite) TestActualDownload404(c *C) {
   231  	n := 0
   232  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   233  		n++
   234  		w.WriteHeader(404)
   235  	}))
   236  	c.Assert(mockServer, NotNil)
   237  	defer mockServer.Close()
   238  
   239  	theStore := store.New(&store.Config{}, nil)
   240  	var buf SillyBuffer
   241  	err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   242  	c.Assert(err, NotNil)
   243  	c.Assert(err, FitsTypeOf, &store.DownloadError{})
   244  	c.Check(err.(*store.DownloadError).Code, Equals, 404)
   245  	c.Check(n, Equals, 1)
   246  }
   247  
   248  func (s *downloadSuite) TestActualDownload500(c *C) {
   249  	n := 0
   250  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   251  		n++
   252  		w.WriteHeader(500)
   253  	}))
   254  	c.Assert(mockServer, NotNil)
   255  	defer mockServer.Close()
   256  
   257  	theStore := store.New(&store.Config{}, nil)
   258  	var buf SillyBuffer
   259  	err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   260  	c.Assert(err, NotNil)
   261  	c.Assert(err, FitsTypeOf, &store.DownloadError{})
   262  	c.Check(err.(*store.DownloadError).Code, Equals, 500)
   263  	c.Check(n, Equals, 5)
   264  }
   265  
   266  func (s *downloadSuite) TestActualDownload500Once(c *C) {
   267  	n := 0
   268  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   269  		n++
   270  		if n == 1 {
   271  			w.WriteHeader(500)
   272  		} else {
   273  			io.WriteString(w, "response-data")
   274  		}
   275  	}))
   276  	c.Assert(mockServer, NotNil)
   277  	defer mockServer.Close()
   278  
   279  	theStore := store.New(&store.Config{}, nil)
   280  	var buf SillyBuffer
   281  	// keep tests happy
   282  	sha3 := ""
   283  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   284  	c.Assert(err, IsNil)
   285  	c.Check(buf.String(), Equals, "response-data")
   286  	c.Check(n, Equals, 2)
   287  }
   288  
   289  // SillyBuffer is a ReadWriteSeeker buffer with a limited size for the tests
   290  // (bytes does not implement an ReadWriteSeeker)
   291  type SillyBuffer struct {
   292  	buf [1024]byte
   293  	pos int64
   294  	end int64
   295  }
   296  
   297  func NewSillyBufferString(s string) *SillyBuffer {
   298  	sb := &SillyBuffer{
   299  		pos: int64(len(s)),
   300  		end: int64(len(s)),
   301  	}
   302  	copy(sb.buf[0:], []byte(s))
   303  	return sb
   304  }
   305  func (sb *SillyBuffer) Read(b []byte) (n int, err error) {
   306  	if sb.pos >= int64(sb.end) {
   307  		return 0, io.EOF
   308  	}
   309  	n = copy(b, sb.buf[sb.pos:sb.end])
   310  	sb.pos += int64(n)
   311  	return n, nil
   312  }
   313  func (sb *SillyBuffer) Seek(offset int64, whence int) (int64, error) {
   314  	if whence != 0 {
   315  		panic("only io.SeekStart implemented in SillyBuffer")
   316  	}
   317  	if offset < 0 || offset > int64(sb.end) {
   318  		return 0, fmt.Errorf("seek out of bounds: %d", offset)
   319  	}
   320  	sb.pos = offset
   321  	return sb.pos, nil
   322  }
   323  func (sb *SillyBuffer) Write(p []byte) (n int, err error) {
   324  	n = copy(sb.buf[sb.pos:], p)
   325  	sb.pos += int64(n)
   326  	if sb.pos > sb.end {
   327  		sb.end = sb.pos
   328  	}
   329  	return n, nil
   330  }
   331  func (sb *SillyBuffer) String() string {
   332  	return string(sb.buf[0:sb.pos])
   333  }
   334  
   335  func (s *downloadSuite) TestActualDownloadResume(c *C) {
   336  	n := 0
   337  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   338  		n++
   339  		w.WriteHeader(206)
   340  		io.WriteString(w, "data")
   341  	}))
   342  	c.Assert(mockServer, NotNil)
   343  	defer mockServer.Close()
   344  
   345  	theStore := store.New(&store.Config{}, nil)
   346  	buf := NewSillyBufferString("some ")
   347  	// calc the expected hash
   348  	h := crypto.SHA3_384.New()
   349  	h.Write([]byte("some data"))
   350  	sha3 := fmt.Sprintf("%x", h.Sum(nil))
   351  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil)
   352  	c.Check(err, IsNil)
   353  	c.Check(buf.String(), Equals, "some data")
   354  	c.Check(n, Equals, 1)
   355  }
   356  
   357  func (s *downloadSuite) TestActualDownloadServerNoResumeHandeled(c *C) {
   358  	n := 0
   359  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   360  		n++
   361  
   362  		switch n {
   363  		case 1:
   364  			c.Check(r.Header["Range"], HasLen, 1)
   365  		default:
   366  			c.Fatal("only one request expected")
   367  		}
   368  		// server does not do partial content and sends full data instead
   369  		w.WriteHeader(200)
   370  		io.WriteString(w, "some data")
   371  	}))
   372  	c.Assert(mockServer, NotNil)
   373  	defer mockServer.Close()
   374  
   375  	theStore := store.New(&store.Config{}, nil)
   376  	buf := NewSillyBufferString("some ")
   377  	// calc the expected hash
   378  	h := crypto.SHA3_384.New()
   379  	h.Write([]byte("some data"))
   380  	sha3 := fmt.Sprintf("%x", h.Sum(nil))
   381  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil)
   382  	c.Check(err, IsNil)
   383  	c.Check(buf.String(), Equals, "some data")
   384  	c.Check(n, Equals, 1)
   385  }
   386  
   387  func (s *downloadSuite) TestUseDeltas(c *C) {
   388  	origPath := os.Getenv("PATH")
   389  	defer os.Setenv("PATH", origPath)
   390  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
   391  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
   392  	restore := release.MockOnClassic(false)
   393  	defer restore()
   394  	altPath := c.MkDir()
   395  	origSnapMountDir := dirs.SnapMountDir
   396  	defer func() { dirs.SnapMountDir = origSnapMountDir }()
   397  	dirs.SnapMountDir = c.MkDir()
   398  	exeInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/usr/bin/xdelta3")
   399  	os.MkdirAll(filepath.Dir(exeInCorePath), 0755)
   400  
   401  	scenarios := []struct {
   402  		env       string
   403  		classic   bool
   404  		exeInHost bool
   405  		exeInCore bool
   406  
   407  		wantDelta bool
   408  	}{
   409  		{env: "", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
   410  		{env: "", classic: false, exeInHost: false, exeInCore: true, wantDelta: true},
   411  		{env: "", classic: false, exeInHost: true, exeInCore: false, wantDelta: true},
   412  		{env: "", classic: false, exeInHost: true, exeInCore: true, wantDelta: true},
   413  		{env: "", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
   414  		{env: "", classic: true, exeInHost: false, exeInCore: true, wantDelta: true},
   415  		{env: "", classic: true, exeInHost: true, exeInCore: false, wantDelta: true},
   416  		{env: "", classic: true, exeInHost: true, exeInCore: true, wantDelta: true},
   417  
   418  		{env: "0", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
   419  		{env: "0", classic: false, exeInHost: false, exeInCore: true, wantDelta: false},
   420  		{env: "0", classic: false, exeInHost: true, exeInCore: false, wantDelta: false},
   421  		{env: "0", classic: false, exeInHost: true, exeInCore: true, wantDelta: false},
   422  		{env: "0", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
   423  		{env: "0", classic: true, exeInHost: false, exeInCore: true, wantDelta: false},
   424  		{env: "0", classic: true, exeInHost: true, exeInCore: false, wantDelta: false},
   425  		{env: "0", classic: true, exeInHost: true, exeInCore: true, wantDelta: false},
   426  
   427  		{env: "1", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
   428  		{env: "1", classic: false, exeInHost: false, exeInCore: true, wantDelta: true},
   429  		{env: "1", classic: false, exeInHost: true, exeInCore: false, wantDelta: true},
   430  		{env: "1", classic: false, exeInHost: true, exeInCore: true, wantDelta: true},
   431  		{env: "1", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
   432  		{env: "1", classic: true, exeInHost: false, exeInCore: true, wantDelta: true},
   433  		{env: "1", classic: true, exeInHost: true, exeInCore: false, wantDelta: true},
   434  		{env: "1", classic: true, exeInHost: true, exeInCore: true, wantDelta: true},
   435  	}
   436  
   437  	for _, scenario := range scenarios {
   438  		if scenario.exeInCore {
   439  			osutil.CopyFile("/bin/true", exeInCorePath, 0)
   440  		} else {
   441  			os.Remove(exeInCorePath)
   442  		}
   443  		os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", scenario.env)
   444  		release.MockOnClassic(scenario.classic)
   445  		if scenario.exeInHost {
   446  			os.Setenv("PATH", origPath)
   447  		} else {
   448  			os.Setenv("PATH", altPath)
   449  		}
   450  
   451  		c.Check(store.UseDeltas(), Equals, scenario.wantDelta, Commentf("%#v", scenario))
   452  	}
   453  }
   454  
   455  type downloadBehaviour []struct {
   456  	url   string
   457  	error bool
   458  }
   459  
   460  var deltaTests = []struct {
   461  	downloads       downloadBehaviour
   462  	info            snap.DownloadInfo
   463  	expectedContent string
   464  }{{
   465  	// The full snap is not downloaded, but rather the delta
   466  	// is downloaded and applied.
   467  	downloads: downloadBehaviour{
   468  		{url: "delta-url"},
   469  	},
   470  	info: snap.DownloadInfo{
   471  		AnonDownloadURL: "full-snap-url",
   472  		Deltas: []snap.DeltaInfo{
   473  			{AnonDownloadURL: "delta-url", Format: "xdelta3"},
   474  		},
   475  	},
   476  	expectedContent: "snap-content-via-delta",
   477  }, {
   478  	// If there is an error during the delta download, the
   479  	// full snap is downloaded as per normal.
   480  	downloads: downloadBehaviour{
   481  		{error: true},
   482  		{url: "full-snap-url"},
   483  	},
   484  	info: snap.DownloadInfo{
   485  		AnonDownloadURL: "full-snap-url",
   486  		Deltas: []snap.DeltaInfo{
   487  			{AnonDownloadURL: "delta-url", Format: "xdelta3"},
   488  		},
   489  	},
   490  	expectedContent: "full-snap-url-content",
   491  }, {
   492  	// If more than one matching delta is returned by the store
   493  	// we ignore deltas and do the full download.
   494  	downloads: downloadBehaviour{
   495  		{url: "full-snap-url"},
   496  	},
   497  	info: snap.DownloadInfo{
   498  		AnonDownloadURL: "full-snap-url",
   499  		Deltas: []snap.DeltaInfo{
   500  			{AnonDownloadURL: "delta-url", Format: "xdelta3"},
   501  			{AnonDownloadURL: "delta-url-2", Format: "xdelta3"},
   502  		},
   503  	},
   504  	expectedContent: "full-snap-url-content",
   505  }}
   506  
   507  func (s *downloadSuite) TestDownloadWithDelta(c *C) {
   508  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
   509  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
   510  	c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
   511  
   512  	for _, testCase := range deltaTests {
   513  		testCase.info.Size = int64(len(testCase.expectedContent))
   514  		downloadIndex := 0
   515  		restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error {
   516  			if testCase.downloads[downloadIndex].error {
   517  				downloadIndex++
   518  				return errors.New("Bang")
   519  			}
   520  			c.Check(url, Equals, testCase.downloads[downloadIndex].url)
   521  			w.Write([]byte(testCase.downloads[downloadIndex].url + "-content"))
   522  			downloadIndex++
   523  			return nil
   524  		})
   525  		defer restore()
   526  		restore = store.MockApplyDelta(func(name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error {
   527  			c.Check(deltaInfo, Equals, &testCase.info.Deltas[0])
   528  			err := ioutil.WriteFile(targetPath, []byte("snap-content-via-delta"), 0644)
   529  			c.Assert(err, IsNil)
   530  			return nil
   531  		})
   532  		defer restore()
   533  
   534  		theStore := store.New(&store.Config{}, nil)
   535  		path := filepath.Join(c.MkDir(), "subdir", "downloaded-file")
   536  		err := theStore.Download(context.TODO(), "foo", path, &testCase.info, nil, nil, nil)
   537  
   538  		c.Assert(err, IsNil)
   539  		defer os.Remove(path)
   540  		c.Assert(path, testutil.FileEquals, testCase.expectedContent)
   541  	}
   542  }
   543  
   544  func (s *downloadSuite) TestActualDownloadRateLimited(c *C) {
   545  	var ratelimitReaderUsed bool
   546  	restore := store.MockRatelimitReader(func(r io.Reader, bucket *ratelimit.Bucket) io.Reader {
   547  		ratelimitReaderUsed = true
   548  		return r
   549  	})
   550  	defer restore()
   551  
   552  	canary := "downloaded data"
   553  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   554  		fmt.Fprint(w, canary)
   555  	}))
   556  	defer ts.Close()
   557  
   558  	theStore := store.New(&store.Config{}, nil)
   559  	var buf SillyBuffer
   560  	err := store.Download(context.TODO(), "example-name", "", ts.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{RateLimit: 1})
   561  	c.Assert(err, IsNil)
   562  	c.Check(buf.String(), Equals, canary)
   563  	c.Check(ratelimitReaderUsed, Equals, true)
   564  }