gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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  	"os/exec"
    34  	"path/filepath"
    35  	"time"
    36  
    37  	"github.com/juju/ratelimit"
    38  	. "gopkg.in/check.v1"
    39  	"gopkg.in/retry.v1"
    40  
    41  	"github.com/snapcore/snapd/dirs"
    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  	mockXdelta *testutil.MockCmd
    52  
    53  	testutil.BaseTest
    54  }
    55  
    56  var _ = Suite(&downloadSuite{})
    57  
    58  func (s *downloadSuite) SetUpTest(c *C) {
    59  	s.BaseTest.SetUpTest(c)
    60  
    61  	store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.Exponential{
    62  		Initial: time.Millisecond,
    63  		Factor:  2.5,
    64  	}))
    65  
    66  	s.mockXdelta = testutil.MockCommand(c, "xdelta3", "")
    67  	s.AddCleanup(s.mockXdelta.Restore)
    68  }
    69  
    70  func (s *downloadSuite) TestActualDownload(c *C) {
    71  	n := 0
    72  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    73  		c.Check(r.Header.Get("Snap-CDN"), Equals, "")
    74  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "")
    75  		n++
    76  		io.WriteString(w, "response-data")
    77  	}))
    78  	c.Assert(mockServer, NotNil)
    79  	defer mockServer.Close()
    80  
    81  	theStore := store.New(&store.Config{}, nil)
    82  	var buf SillyBuffer
    83  	// keep tests happy
    84  	sha3 := ""
    85  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
    86  	c.Assert(err, IsNil)
    87  	c.Check(buf.String(), Equals, "response-data")
    88  	c.Check(n, Equals, 1)
    89  }
    90  
    91  func (s *downloadSuite) TestActualDownloadAutoRefresh(c *C) {
    92  	n := 0
    93  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    94  		c.Check(r.Header.Get("Snap-Refresh-Reason"), Equals, "scheduled")
    95  		n++
    96  		io.WriteString(w, "response-data")
    97  	}))
    98  	c.Assert(mockServer, NotNil)
    99  	defer mockServer.Close()
   100  
   101  	theStore := store.New(&store.Config{}, nil)
   102  	var buf SillyBuffer
   103  	// keep tests happy
   104  	sha3 := ""
   105  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{IsAutoRefresh: true})
   106  	c.Assert(err, IsNil)
   107  	c.Check(buf.String(), Equals, "response-data")
   108  	c.Check(n, Equals, 1)
   109  }
   110  
   111  func (s *downloadSuite) TestActualDownloadNoCDN(c *C) {
   112  	os.Setenv("SNAPPY_STORE_NO_CDN", "1")
   113  	defer os.Unsetenv("SNAPPY_STORE_NO_CDN")
   114  
   115  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   116  		c.Check(r.Header.Get("Snap-CDN"), Equals, "none")
   117  		io.WriteString(w, "response-data")
   118  	}))
   119  	c.Assert(mockServer, NotNil)
   120  	defer mockServer.Close()
   121  
   122  	theStore := store.New(&store.Config{}, nil)
   123  	var buf SillyBuffer
   124  	// keep tests happy
   125  	sha3 := ""
   126  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   127  	c.Assert(err, IsNil)
   128  	c.Check(buf.String(), Equals, "response-data")
   129  }
   130  
   131  func (s *downloadSuite) TestActualDownloadFullCloudInfoFromAuthContext(c *C) {
   132  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   133  		c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="aws" region="us-east-1" availability-zone="us-east-1c"`)
   134  
   135  		io.WriteString(w, "response-data")
   136  	}))
   137  	c.Assert(mockServer, NotNil)
   138  	defer mockServer.Close()
   139  
   140  	device := createTestDevice()
   141  	theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "aws", Region: "us-east-1", AvailabilityZone: "us-east-1c"}})
   142  
   143  	var buf SillyBuffer
   144  	// keep tests happy
   145  	sha3 := ""
   146  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   147  	c.Assert(err, IsNil)
   148  	c.Check(buf.String(), Equals, "response-data")
   149  }
   150  
   151  func (s *downloadSuite) TestActualDownloadLessDetailedCloudInfoFromAuthContext(c *C) {
   152  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   153  		c.Check(r.Header.Get("Snap-CDN"), Equals, `cloud-name="openstack" availability-zone="nova"`)
   154  
   155  		io.WriteString(w, "response-data")
   156  	}))
   157  	c.Assert(mockServer, NotNil)
   158  	defer mockServer.Close()
   159  
   160  	device := createTestDevice()
   161  	theStore := store.New(&store.Config{}, &testDauthContext{c: c, device: device, cloudInfo: &auth.CloudInfo{Name: "openstack", Region: "", AvailabilityZone: "nova"}})
   162  
   163  	var buf SillyBuffer
   164  	// keep tests happy
   165  	sha3 := ""
   166  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   167  	c.Assert(err, IsNil)
   168  	c.Check(buf.String(), Equals, "response-data")
   169  }
   170  
   171  func (s *downloadSuite) TestDownloadCancellation(c *C) {
   172  	// the channel used by mock server to request cancellation from the test
   173  	syncCh := make(chan struct{})
   174  
   175  	n := 0
   176  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   177  		n++
   178  		io.WriteString(w, "foo")
   179  		syncCh <- struct{}{}
   180  		io.WriteString(w, "bar")
   181  		time.Sleep(10 * time.Millisecond)
   182  	}))
   183  	c.Assert(mockServer, NotNil)
   184  	defer mockServer.Close()
   185  
   186  	theStore := store.New(&store.Config{}, nil)
   187  
   188  	ctx, cancel := context.WithCancel(context.Background())
   189  
   190  	result := make(chan string)
   191  	go func() {
   192  		sha3 := ""
   193  		var buf SillyBuffer
   194  		err := store.Download(ctx, "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   195  		result <- err.Error()
   196  		close(result)
   197  	}()
   198  
   199  	<-syncCh
   200  	cancel()
   201  
   202  	err := <-result
   203  	c.Check(n, Equals, 1)
   204  	c.Assert(err, Equals, "the download has been cancelled: context canceled")
   205  }
   206  
   207  type nopeSeeker struct{ io.ReadWriter }
   208  
   209  func (nopeSeeker) Seek(int64, int) (int64, error) {
   210  	return -1, errors.New("what is this, quidditch?")
   211  }
   212  
   213  func (s *downloadSuite) TestActualDownloadNonPurchased402(c *C) {
   214  	n := 0
   215  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   216  		n++
   217  		// XXX: the server doesn't behave correctly ATM
   218  		// but 401 for paid snaps is the unlikely case so far
   219  		w.WriteHeader(402)
   220  	}))
   221  	c.Assert(mockServer, NotNil)
   222  	defer mockServer.Close()
   223  
   224  	theStore := store.New(&store.Config{}, nil)
   225  	var buf bytes.Buffer
   226  	err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, nopeSeeker{&buf}, -1, nil, nil)
   227  	c.Assert(err, NotNil)
   228  	c.Check(err.Error(), Equals, "please buy foo before installing it")
   229  	c.Check(n, Equals, 1)
   230  }
   231  
   232  func (s *downloadSuite) TestActualDownload404(c *C) {
   233  	n := 0
   234  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   235  		n++
   236  		w.WriteHeader(404)
   237  	}))
   238  	c.Assert(mockServer, NotNil)
   239  	defer mockServer.Close()
   240  
   241  	theStore := store.New(&store.Config{}, nil)
   242  	var buf SillyBuffer
   243  	err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   244  	c.Assert(err, NotNil)
   245  	c.Assert(err, FitsTypeOf, &store.DownloadError{})
   246  	c.Check(err.(*store.DownloadError).Code, Equals, 404)
   247  	c.Check(n, Equals, 1)
   248  }
   249  
   250  func (s *downloadSuite) TestActualDownload500(c *C) {
   251  	n := 0
   252  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   253  		n++
   254  		w.WriteHeader(500)
   255  	}))
   256  	c.Assert(mockServer, NotNil)
   257  	defer mockServer.Close()
   258  
   259  	theStore := store.New(&store.Config{}, nil)
   260  	var buf SillyBuffer
   261  	err := store.Download(context.TODO(), "foo", "sha3", mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   262  	c.Assert(err, NotNil)
   263  	c.Assert(err, FitsTypeOf, &store.DownloadError{})
   264  	c.Check(err.(*store.DownloadError).Code, Equals, 500)
   265  	c.Check(n, Equals, 5)
   266  }
   267  
   268  func (s *downloadSuite) TestActualDownload500Once(c *C) {
   269  	n := 0
   270  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   271  		n++
   272  		if n == 1 {
   273  			w.WriteHeader(500)
   274  		} else {
   275  			io.WriteString(w, "response-data")
   276  		}
   277  	}))
   278  	c.Assert(mockServer, NotNil)
   279  	defer mockServer.Close()
   280  
   281  	theStore := store.New(&store.Config{}, nil)
   282  	var buf SillyBuffer
   283  	// keep tests happy
   284  	sha3 := ""
   285  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, &buf, 0, nil, nil)
   286  	c.Assert(err, IsNil)
   287  	c.Check(buf.String(), Equals, "response-data")
   288  	c.Check(n, Equals, 2)
   289  }
   290  
   291  // SillyBuffer is a ReadWriteSeeker buffer with a limited size for the tests
   292  // (bytes does not implement an ReadWriteSeeker)
   293  type SillyBuffer struct {
   294  	buf [1024]byte
   295  	pos int64
   296  	end int64
   297  }
   298  
   299  func NewSillyBufferString(s string) *SillyBuffer {
   300  	sb := &SillyBuffer{
   301  		pos: int64(len(s)),
   302  		end: int64(len(s)),
   303  	}
   304  	copy(sb.buf[0:], []byte(s))
   305  	return sb
   306  }
   307  func (sb *SillyBuffer) Read(b []byte) (n int, err error) {
   308  	if sb.pos >= int64(sb.end) {
   309  		return 0, io.EOF
   310  	}
   311  	n = copy(b, sb.buf[sb.pos:sb.end])
   312  	sb.pos += int64(n)
   313  	return n, nil
   314  }
   315  func (sb *SillyBuffer) Seek(offset int64, whence int) (int64, error) {
   316  	if whence != 0 {
   317  		panic("only io.SeekStart implemented in SillyBuffer")
   318  	}
   319  	if offset < 0 || offset > int64(sb.end) {
   320  		return 0, fmt.Errorf("seek out of bounds: %d", offset)
   321  	}
   322  	sb.pos = offset
   323  	return sb.pos, nil
   324  }
   325  func (sb *SillyBuffer) Write(p []byte) (n int, err error) {
   326  	n = copy(sb.buf[sb.pos:], p)
   327  	sb.pos += int64(n)
   328  	if sb.pos > sb.end {
   329  		sb.end = sb.pos
   330  	}
   331  	return n, nil
   332  }
   333  func (sb *SillyBuffer) String() string {
   334  	return string(sb.buf[0:sb.pos])
   335  }
   336  
   337  func (s *downloadSuite) TestActualDownloadResume(c *C) {
   338  	n := 0
   339  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   340  		n++
   341  		w.WriteHeader(206)
   342  		io.WriteString(w, "data")
   343  	}))
   344  	c.Assert(mockServer, NotNil)
   345  	defer mockServer.Close()
   346  
   347  	theStore := store.New(&store.Config{}, nil)
   348  	buf := NewSillyBufferString("some ")
   349  	// calc the expected hash
   350  	h := crypto.SHA3_384.New()
   351  	h.Write([]byte("some data"))
   352  	sha3 := fmt.Sprintf("%x", h.Sum(nil))
   353  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil)
   354  	c.Check(err, IsNil)
   355  	c.Check(buf.String(), Equals, "some data")
   356  	c.Check(n, Equals, 1)
   357  }
   358  
   359  func (s *downloadSuite) TestActualDownloadServerNoResumeHandeled(c *C) {
   360  	n := 0
   361  	mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   362  		n++
   363  
   364  		switch n {
   365  		case 1:
   366  			c.Check(r.Header["Range"], HasLen, 1)
   367  		default:
   368  			c.Fatal("only one request expected")
   369  		}
   370  		// server does not do partial content and sends full data instead
   371  		w.WriteHeader(200)
   372  		io.WriteString(w, "some data")
   373  	}))
   374  	c.Assert(mockServer, NotNil)
   375  	defer mockServer.Close()
   376  
   377  	theStore := store.New(&store.Config{}, nil)
   378  	buf := NewSillyBufferString("some ")
   379  	// calc the expected hash
   380  	h := crypto.SHA3_384.New()
   381  	h.Write([]byte("some data"))
   382  	sha3 := fmt.Sprintf("%x", h.Sum(nil))
   383  	err := store.Download(context.TODO(), "foo", sha3, mockServer.URL, nil, theStore, buf, int64(len("some ")), nil, nil)
   384  	c.Check(err, IsNil)
   385  	c.Check(buf.String(), Equals, "some data")
   386  	c.Check(n, Equals, 1)
   387  }
   388  
   389  func (s *downloadSuite) TestUseDeltas(c *C) {
   390  	// get rid of the mock xdelta3 because we mock all our own stuff
   391  	s.mockXdelta.Restore()
   392  	origPath := os.Getenv("PATH")
   393  	defer os.Setenv("PATH", origPath)
   394  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
   395  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
   396  	restore := release.MockOnClassic(false)
   397  	defer restore()
   398  
   399  	origSnapMountDir := dirs.SnapMountDir
   400  	defer func() { dirs.SnapMountDir = origSnapMountDir }()
   401  	dirs.SnapMountDir = c.MkDir()
   402  	exeInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/usr/bin/xdelta3")
   403  	interpInCorePath := filepath.Join(dirs.SnapMountDir, "/core/current/lib64/ld-linux-x86-64.so.2")
   404  
   405  	scenarios := []struct {
   406  		env       string
   407  		classic   bool
   408  		exeInHost bool
   409  		exeInCore bool
   410  
   411  		wantDelta bool
   412  	}{
   413  		{env: "", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
   414  		{env: "", classic: false, exeInHost: false, exeInCore: true, wantDelta: true},
   415  		{env: "", classic: false, exeInHost: true, exeInCore: false, wantDelta: true},
   416  		{env: "", classic: false, exeInHost: true, exeInCore: true, wantDelta: true},
   417  		{env: "", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
   418  		{env: "", classic: true, exeInHost: false, exeInCore: true, wantDelta: true},
   419  		{env: "", classic: true, exeInHost: true, exeInCore: false, wantDelta: true},
   420  		{env: "", classic: true, exeInHost: true, exeInCore: true, wantDelta: true},
   421  
   422  		{env: "0", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
   423  		{env: "0", classic: false, exeInHost: false, exeInCore: true, wantDelta: false},
   424  		{env: "0", classic: false, exeInHost: true, exeInCore: false, wantDelta: false},
   425  		{env: "0", classic: false, exeInHost: true, exeInCore: true, wantDelta: false},
   426  		{env: "0", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
   427  		{env: "0", classic: true, exeInHost: false, exeInCore: true, wantDelta: false},
   428  		{env: "0", classic: true, exeInHost: true, exeInCore: false, wantDelta: false},
   429  		{env: "0", classic: true, exeInHost: true, exeInCore: true, wantDelta: false},
   430  
   431  		{env: "1", classic: false, exeInHost: false, exeInCore: false, wantDelta: false},
   432  		{env: "1", classic: false, exeInHost: false, exeInCore: true, wantDelta: true},
   433  		{env: "1", classic: false, exeInHost: true, exeInCore: false, wantDelta: true},
   434  		{env: "1", classic: false, exeInHost: true, exeInCore: true, wantDelta: true},
   435  		{env: "1", classic: true, exeInHost: false, exeInCore: false, wantDelta: false},
   436  		{env: "1", classic: true, exeInHost: false, exeInCore: true, wantDelta: true},
   437  		{env: "1", classic: true, exeInHost: true, exeInCore: false, wantDelta: true},
   438  		{env: "1", classic: true, exeInHost: true, exeInCore: true, wantDelta: true},
   439  	}
   440  
   441  	for _, scenario := range scenarios {
   442  		var hostXdelta3Cmd, coreInterpCmd *testutil.MockCmd
   443  
   444  		var cleanups []func()
   445  
   446  		comment := Commentf("%#v", scenario)
   447  
   448  		// setup the env var for the scenario
   449  		os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", scenario.env)
   450  		release.MockOnClassic(scenario.classic)
   451  
   452  		// setup binaries for the scenario
   453  		if scenario.exeInCore {
   454  			// We need both the xdelta3 command for determining the interpreter
   455  			// as well as the actual interpreter for executing the basic
   456  			// "xdelta3 config" command.
   457  			// For the interpreter, since that's how we execute xdelta3, mock
   458  			// that as a command, but we don't need to mock the xdelta3 command
   459  			// in the core snap since that doesn't get executed by our fake
   460  			// interpreter. Mocking the interpreter and executing that as a
   461  			// MockCommand has the advantage that it avoids the specific ELF
   462  			// handling that is per-arch, etc. of the real CommandFromSystemSnap
   463  			// implementation.
   464  
   465  			coreInterpCmd = testutil.MockCommand(c, interpInCorePath, "")
   466  
   467  			r := store.MockSnapdtoolCommandFromSystemSnap(func(name string, args ...string) (*exec.Cmd, error) {
   468  				c.Assert(name, Equals, "/usr/bin/xdelta3")
   469  				c.Assert(args, DeepEquals, []string{"config"})
   470  
   471  				// use realistic arguments like what we actually get from
   472  				// snapdtool.CommandFromSystemSnap(), namely the interpreter and
   473  				// a library path which is derived from ld.so - this is
   474  				// artificial and we could use any mocked arguments here, but
   475  				// this more closely matches reality to return something like
   476  				// this.
   477  				interpArgs := append([]string{"--library-path", "/some/dir/from/etc/ld.so", exeInCorePath}, args...)
   478  				return exec.Command(coreInterpCmd.Exe(), interpArgs...), nil
   479  			})
   480  			cleanups = append(cleanups, r)
   481  
   482  			// Forget the calls to the interpreter at the end of the test - this
   483  			// deletes the log which otherwise would  continue to persist for
   484  			// each iteration leading to incorrect checks for the calls to the
   485  			// absolute binary that we mocked here, as the log file will be the
   486  			// same for each iteration.
   487  			// For the inverse reason, we don't need to forget calls for the
   488  			// hostXdelta3Cmd mock command, it gets a new dir with a new log
   489  			// file each iteration.
   490  			cleanups = append(cleanups, func() {
   491  				coreInterpCmd.ForgetCalls()
   492  				// note this is currently not needed, since Restore() just
   493  				// resets $PATH, but for an absolute path the $PATH doesn't get
   494  				// modified to begin with in MockCommand, but keep it here just
   495  				// to be safe in case something does ever change
   496  				coreInterpCmd.Restore()
   497  
   498  			})
   499  		}
   500  
   501  		if scenario.exeInHost {
   502  			// just mock the xdelta3 command directly
   503  			hostXdelta3Cmd = testutil.MockCommand(c, "xdelta3", "")
   504  
   505  			// note we don't add a Restore() to cleanups, it is called directly
   506  			// below after the first UseDeltas() but before the second
   507  			// UseDeltas() in order to properly test the caching behavior
   508  		}
   509  
   510  		// if there is not meant to be xdelta3 on the host or in core, then set
   511  		// PATH to be empty such that we won't find xdelta3 from the host
   512  		// running these tests
   513  		if !scenario.exeInHost && !scenario.exeInCore {
   514  			os.Setenv("PATH", "")
   515  
   516  			// also reset PATH at the end, otherwise an empty PATH leads
   517  			// testutil.MockCommand fails in future iterations that mock a
   518  			// command
   519  			cleanups = append(cleanups, func() {
   520  				os.Setenv("PATH", origPath)
   521  			})
   522  		}
   523  
   524  		// run the check for delta usage, we call it twice
   525  		sto := &store.Store{}
   526  		c.Check(sto.UseDeltas(), Equals, scenario.wantDelta, comment)
   527  
   528  		// cleanup the files we may have created before calling the function
   529  		// again to ensure that the caching works as expected
   530  		if scenario.exeInCore {
   531  			err := os.Remove(interpInCorePath)
   532  			c.Assert(err, IsNil)
   533  		}
   534  
   535  		if scenario.exeInHost {
   536  			hostXdelta3Cmd.Restore()
   537  		}
   538  
   539  		// also now that we have deleted the mock interpreter and unset the
   540  		// search path, we should still get the same result as above when
   541  		// we call UseDeltas() since it was cached, if it wasn't cached then
   542  		// this would fail
   543  		c.Check(sto.UseDeltas(), Equals, scenario.wantDelta, comment)
   544  
   545  		if scenario.wantDelta {
   546  			// if we should have been able to use deltas, make sure we picked
   547  			// the expected one, - if both were true we should have picked the
   548  			// one from core instead of the one from the host first
   549  			if scenario.exeInCore {
   550  				// check that during trying to check whether to use deltas or
   551  				// not, we called the interpreter with the xdelta3 config
   552  				// command too
   553  				c.Check(coreInterpCmd.Calls(), DeepEquals, [][]string{
   554  					{"ld-linux-x86-64.so.2", "--library-path", "/some/dir/from/etc/ld.so", exeInCorePath, "config"},
   555  				}, comment)
   556  
   557  				// also check that now after caching the xdelta3 command, it
   558  				// returns the expected format
   559  				expArgs := []string{
   560  					interpInCorePath,
   561  					"--library-path",
   562  					"/some/dir/from/etc/ld.so",
   563  					exeInCorePath,
   564  					"foo",
   565  					"bar",
   566  				}
   567  				// check that the Xdelta3Cmd function we cached uses the
   568  				// interpreter that was returned from CommandFromSystemSnap
   569  				c.Check(sto.Xdelta3Cmd("foo", "bar").Args, DeepEquals, expArgs, comment)
   570  
   571  			} else if scenario.exeInHost {
   572  				// similar checks for the host case, except in the host case we
   573  				// just called xdelta3 directly
   574  				c.Check(hostXdelta3Cmd.Calls(), DeepEquals, [][]string{
   575  					{"xdelta3", "config"},
   576  				}, comment)
   577  
   578  				// and args are passed to the command cached too
   579  				expArgs := []string{hostXdelta3Cmd.Exe(), "foo", "bar"}
   580  				c.Check(sto.Xdelta3Cmd("foo", "bar").Args, DeepEquals, expArgs, comment)
   581  			}
   582  		} else {
   583  			// sanity check that the test case makes sense, if we didn't want
   584  			// deltas, the scenario should have either disabled via an env var,
   585  			// or had both exes missing
   586  			c.Assert((scenario.env == "0") ||
   587  				(!scenario.exeInCore && !scenario.exeInHost),
   588  				Equals, true)
   589  		}
   590  
   591  		// cleanup for the next iteration
   592  		for _, r := range cleanups {
   593  			r()
   594  		}
   595  	}
   596  }
   597  
   598  type downloadBehaviour []struct {
   599  	url   string
   600  	error bool
   601  }
   602  
   603  var deltaTests = []struct {
   604  	downloads       downloadBehaviour
   605  	info            snap.DownloadInfo
   606  	expectedContent string
   607  }{{
   608  	// The full snap is not downloaded, but rather the delta
   609  	// is downloaded and applied.
   610  	downloads: downloadBehaviour{
   611  		{url: "delta-url"},
   612  	},
   613  	info: snap.DownloadInfo{
   614  		AnonDownloadURL: "full-snap-url",
   615  		Deltas: []snap.DeltaInfo{
   616  			{AnonDownloadURL: "delta-url", Format: "xdelta3"},
   617  		},
   618  	},
   619  	expectedContent: "snap-content-via-delta",
   620  }, {
   621  	// If there is an error during the delta download, the
   622  	// full snap is downloaded as per normal.
   623  	downloads: downloadBehaviour{
   624  		{error: true},
   625  		{url: "full-snap-url"},
   626  	},
   627  	info: snap.DownloadInfo{
   628  		AnonDownloadURL: "full-snap-url",
   629  		Deltas: []snap.DeltaInfo{
   630  			{AnonDownloadURL: "delta-url", Format: "xdelta3"},
   631  		},
   632  	},
   633  	expectedContent: "full-snap-url-content",
   634  }, {
   635  	// If more than one matching delta is returned by the store
   636  	// we ignore deltas and do the full download.
   637  	downloads: downloadBehaviour{
   638  		{url: "full-snap-url"},
   639  	},
   640  	info: snap.DownloadInfo{
   641  		AnonDownloadURL: "full-snap-url",
   642  		Deltas: []snap.DeltaInfo{
   643  			{AnonDownloadURL: "delta-url", Format: "xdelta3"},
   644  			{AnonDownloadURL: "delta-url-2", Format: "xdelta3"},
   645  		},
   646  	},
   647  	expectedContent: "full-snap-url-content",
   648  }}
   649  
   650  func (s *downloadSuite) TestDownloadWithDelta(c *C) {
   651  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
   652  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
   653  	c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
   654  
   655  	for _, testCase := range deltaTests {
   656  		testCase.info.Size = int64(len(testCase.expectedContent))
   657  		downloadIndex := 0
   658  		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 {
   659  			if testCase.downloads[downloadIndex].error {
   660  				downloadIndex++
   661  				return errors.New("Bang")
   662  			}
   663  			c.Check(url, Equals, testCase.downloads[downloadIndex].url)
   664  			w.Write([]byte(testCase.downloads[downloadIndex].url + "-content"))
   665  			downloadIndex++
   666  			return nil
   667  		})
   668  		defer restore()
   669  		restore = store.MockApplyDelta(func(_ *store.Store, name string, deltaPath string, deltaInfo *snap.DeltaInfo, targetPath string, targetSha3_384 string) error {
   670  			c.Check(deltaInfo, Equals, &testCase.info.Deltas[0])
   671  			err := ioutil.WriteFile(targetPath, []byte("snap-content-via-delta"), 0644)
   672  			c.Assert(err, IsNil)
   673  			return nil
   674  		})
   675  		defer restore()
   676  
   677  		theStore := store.New(&store.Config{}, nil)
   678  		path := filepath.Join(c.MkDir(), "subdir", "downloaded-file")
   679  		err := theStore.Download(context.TODO(), "foo", path, &testCase.info, nil, nil, nil)
   680  
   681  		c.Assert(err, IsNil)
   682  		defer os.Remove(path)
   683  		c.Assert(path, testutil.FileEquals, testCase.expectedContent)
   684  	}
   685  }
   686  
   687  func (s *downloadSuite) TestActualDownloadRateLimited(c *C) {
   688  	var ratelimitReaderUsed bool
   689  	restore := store.MockRatelimitReader(func(r io.Reader, bucket *ratelimit.Bucket) io.Reader {
   690  		ratelimitReaderUsed = true
   691  		return r
   692  	})
   693  	defer restore()
   694  
   695  	canary := "downloaded data"
   696  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   697  		fmt.Fprint(w, canary)
   698  	}))
   699  	defer ts.Close()
   700  
   701  	theStore := store.New(&store.Config{}, nil)
   702  	var buf SillyBuffer
   703  	err := store.Download(context.TODO(), "example-name", "", ts.URL, nil, theStore, &buf, 0, nil, &store.DownloadOptions{RateLimit: 1})
   704  	c.Assert(err, IsNil)
   705  	c.Check(buf.String(), Equals, canary)
   706  	c.Check(ratelimitReaderUsed, Equals, true)
   707  }