github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/store/store_download_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2020 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  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"net/http"
    30  	"net/http/httptest"
    31  	"net/url"
    32  	"os"
    33  	"path/filepath"
    34  	"time"
    35  
    36  	"golang.org/x/crypto/sha3"
    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/snap"
    45  	"github.com/snapcore/snapd/store"
    46  	"github.com/snapcore/snapd/testutil"
    47  )
    48  
    49  type storeDownloadSuite struct {
    50  	baseStoreSuite
    51  
    52  	store *store.Store
    53  
    54  	localUser *auth.UserState
    55  
    56  	mockXDelta *testutil.MockCmd
    57  }
    58  
    59  var _ = Suite(&storeDownloadSuite{})
    60  
    61  func (s *storeDownloadSuite) SetUpTest(c *C) {
    62  	s.baseStoreSuite.SetUpTest(c)
    63  
    64  	c.Assert(os.MkdirAll(dirs.SnapMountDir, 0755), IsNil)
    65  
    66  	s.store = store.New(nil, nil)
    67  
    68  	s.localUser = &auth.UserState{
    69  		ID:       11,
    70  		Username: "test-user",
    71  		Macaroon: "snapd-macaroon",
    72  	}
    73  
    74  	s.mockXDelta = testutil.MockCommand(c, "xdelta3", "")
    75  	s.AddCleanup(s.mockXDelta.Restore)
    76  
    77  	store.MockDownloadRetryStrategy(&s.BaseTest, retry.LimitCount(5, retry.LimitTime(1*time.Second,
    78  		retry.Exponential{
    79  			Initial: 1 * time.Millisecond,
    80  			Factor:  1,
    81  		},
    82  	)))
    83  }
    84  
    85  func (s *storeDownloadSuite) TestDownloadOK(c *C) {
    86  	expectedContent := []byte("I was downloaded")
    87  
    88  	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 {
    89  		c.Check(url, Equals, "anon-url")
    90  		w.Write(expectedContent)
    91  		return nil
    92  	})
    93  	defer restore()
    94  
    95  	snap := &snap.Info{}
    96  	snap.RealName = "foo"
    97  	snap.AnonDownloadURL = "anon-url"
    98  	snap.DownloadURL = "AUTH-URL"
    99  	snap.Size = int64(len(expectedContent))
   100  
   101  	path := filepath.Join(c.MkDir(), "downloaded-file")
   102  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil)
   103  	c.Assert(err, IsNil)
   104  	defer os.Remove(path)
   105  
   106  	c.Assert(path, testutil.FileEquals, expectedContent)
   107  }
   108  
   109  func (s *storeDownloadSuite) TestDownloadRangeRequest(c *C) {
   110  	partialContentStr := "partial content "
   111  	missingContentStr := "was downloaded"
   112  	expectedContentStr := partialContentStr + missingContentStr
   113  
   114  	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 {
   115  		c.Check(resume, Equals, int64(len(partialContentStr)))
   116  		c.Check(url, Equals, "anon-url")
   117  		w.Write([]byte(missingContentStr))
   118  		return nil
   119  	})
   120  	defer restore()
   121  
   122  	snap := &snap.Info{}
   123  	snap.RealName = "foo"
   124  	snap.AnonDownloadURL = "anon-url"
   125  	snap.DownloadURL = "AUTH-URL"
   126  	snap.Sha3_384 = "abcdabcd"
   127  	snap.Size = int64(len(expectedContentStr))
   128  
   129  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   130  	err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
   131  	c.Assert(err, IsNil)
   132  
   133  	err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   134  	c.Assert(err, IsNil)
   135  
   136  	c.Assert(targetFn, testutil.FileEquals, expectedContentStr)
   137  }
   138  
   139  func (s *storeDownloadSuite) TestResumeOfCompleted(c *C) {
   140  	expectedContentStr := "nothing downloaded"
   141  
   142  	snap := &snap.Info{}
   143  	snap.RealName = "foo"
   144  	snap.AnonDownloadURL = "anon-url"
   145  	snap.DownloadURL = "AUTH-URL"
   146  	snap.Sha3_384 = fmt.Sprintf("%x", sha3.Sum384([]byte(expectedContentStr)))
   147  	snap.Size = int64(len(expectedContentStr))
   148  
   149  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   150  	err := ioutil.WriteFile(targetFn+".partial", []byte(expectedContentStr), 0644)
   151  	c.Assert(err, IsNil)
   152  
   153  	err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   154  	c.Assert(err, IsNil)
   155  
   156  	c.Assert(targetFn, testutil.FileEquals, expectedContentStr)
   157  }
   158  
   159  func (s *storeDownloadSuite) TestDownloadEOFHandlesResumeHashCorrectly(c *C) {
   160  	n := 0
   161  	var mockServer *httptest.Server
   162  
   163  	// our mock download content
   164  	buf := make([]byte, 50000)
   165  	for i := range buf {
   166  		buf[i] = 'x'
   167  	}
   168  	h := crypto.SHA3_384.New()
   169  	io.Copy(h, bytes.NewBuffer(buf))
   170  
   171  	// raise an EOF shortly before the end
   172  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   173  		n++
   174  		if n < 2 {
   175  			w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf)))
   176  			w.Write(buf[0 : len(buf)-5])
   177  			mockServer.CloseClientConnections()
   178  			return
   179  		}
   180  		if len(r.Header["Range"]) > 0 {
   181  			w.WriteHeader(206)
   182  		}
   183  		w.Write(buf[len(buf)-5:])
   184  	}))
   185  
   186  	c.Assert(mockServer, NotNil)
   187  	defer mockServer.Close()
   188  
   189  	snap := &snap.Info{}
   190  	snap.RealName = "foo"
   191  	snap.AnonDownloadURL = mockServer.URL
   192  	snap.DownloadURL = "AUTH-URL"
   193  	snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
   194  	snap.Size = 50000
   195  
   196  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   197  	err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   198  	c.Assert(err, IsNil)
   199  	c.Assert(targetFn, testutil.FileEquals, buf)
   200  	c.Assert(s.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*")
   201  }
   202  
   203  func (s *storeDownloadSuite) TestDownloadRetryHashErrorIsFullyRetried(c *C) {
   204  	n := 0
   205  	var mockServer *httptest.Server
   206  
   207  	// our mock download content
   208  	buf := make([]byte, 50000)
   209  	for i := range buf {
   210  		buf[i] = 'x'
   211  	}
   212  	h := crypto.SHA3_384.New()
   213  	io.Copy(h, bytes.NewBuffer(buf))
   214  
   215  	// raise an EOF shortly before the end and send the WRONG content next
   216  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   217  		n++
   218  		switch n {
   219  		case 1:
   220  			w.Header().Add("Content-Length", fmt.Sprintf("%d", len(buf)))
   221  			w.Write(buf[0 : len(buf)-5])
   222  			mockServer.CloseClientConnections()
   223  		case 2:
   224  			io.WriteString(w, "yyyyy")
   225  		case 3:
   226  			w.Write(buf)
   227  		}
   228  	}))
   229  
   230  	c.Assert(mockServer, NotNil)
   231  	defer mockServer.Close()
   232  
   233  	snap := &snap.Info{}
   234  	snap.RealName = "foo"
   235  	snap.AnonDownloadURL = mockServer.URL
   236  	snap.DownloadURL = "AUTH-URL"
   237  	snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
   238  	snap.Size = 50000
   239  
   240  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   241  	err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   242  	c.Assert(err, IsNil)
   243  
   244  	c.Assert(targetFn, testutil.FileEquals, buf)
   245  
   246  	c.Assert(s.logbuf.String(), Matches, "(?s).*Retrying .* attempt 2, .*")
   247  }
   248  
   249  func (s *storeDownloadSuite) TestResumeOfCompletedRetriedOnHashFailure(c *C) {
   250  	var mockServer *httptest.Server
   251  
   252  	// our mock download content
   253  	buf := make([]byte, 50000)
   254  	badbuf := make([]byte, 50000)
   255  	for i := range buf {
   256  		buf[i] = 'x'
   257  		badbuf[i] = 'y'
   258  	}
   259  	h := crypto.SHA3_384.New()
   260  	io.Copy(h, bytes.NewBuffer(buf))
   261  
   262  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   263  		w.Write(buf)
   264  	}))
   265  
   266  	c.Assert(mockServer, NotNil)
   267  	defer mockServer.Close()
   268  
   269  	snap := &snap.Info{}
   270  	snap.RealName = "foo"
   271  	snap.AnonDownloadURL = mockServer.URL
   272  	snap.DownloadURL = "AUTH-URL"
   273  	snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
   274  	snap.Size = 50000
   275  
   276  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   277  	c.Assert(ioutil.WriteFile(targetFn+".partial", badbuf, 0644), IsNil)
   278  	err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   279  	c.Assert(err, IsNil)
   280  
   281  	c.Assert(targetFn, testutil.FileEquals, buf)
   282  
   283  	c.Assert(s.logbuf.String(), Matches, "(?s).*sha3-384 mismatch.*")
   284  }
   285  
   286  func (s *storeDownloadSuite) TestResumeOfTooMuchDataWorks(c *C) {
   287  	var mockServer *httptest.Server
   288  
   289  	// our mock download content
   290  	snapContent := "snap-content"
   291  	// the partial file has too much data
   292  	tooMuchLocalData := "way-way-way-too-much-snap-content"
   293  
   294  	h := crypto.SHA3_384.New()
   295  	io.Copy(h, bytes.NewBufferString(snapContent))
   296  
   297  	n := 0
   298  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   299  		n++
   300  		w.Write([]byte(snapContent))
   301  	}))
   302  	c.Assert(mockServer, NotNil)
   303  	defer mockServer.Close()
   304  
   305  	snap := &snap.Info{}
   306  	snap.RealName = "foo"
   307  	snap.AnonDownloadURL = mockServer.URL
   308  	snap.DownloadURL = "AUTH-URL"
   309  	snap.Sha3_384 = fmt.Sprintf("%x", h.Sum(nil))
   310  	snap.Size = int64(len(snapContent))
   311  
   312  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   313  	c.Assert(ioutil.WriteFile(targetFn+".partial", []byte(tooMuchLocalData), 0644), IsNil)
   314  	err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   315  	c.Assert(err, IsNil)
   316  	c.Assert(n, Equals, 1)
   317  
   318  	c.Assert(targetFn, testutil.FileEquals, snapContent)
   319  
   320  	c.Assert(s.logbuf.String(), Matches, "(?s).*sha3-384 mismatch.*")
   321  }
   322  
   323  func (s *storeDownloadSuite) TestDownloadRetryHashErrorIsFullyRetriedOnlyOnce(c *C) {
   324  	n := 0
   325  	var mockServer *httptest.Server
   326  
   327  	mockServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   328  		n++
   329  		io.WriteString(w, "something invalid")
   330  	}))
   331  
   332  	c.Assert(mockServer, NotNil)
   333  	defer mockServer.Close()
   334  
   335  	snap := &snap.Info{}
   336  	snap.RealName = "foo"
   337  	snap.AnonDownloadURL = mockServer.URL
   338  	snap.DownloadURL = "AUTH-URL"
   339  	snap.Sha3_384 = "invalid-hash"
   340  	snap.Size = int64(len("something invalid"))
   341  
   342  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   343  	err := s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   344  
   345  	_, ok := err.(store.HashError)
   346  	c.Assert(ok, Equals, true)
   347  	// ensure we only retried once (as these downloads might be big)
   348  	c.Assert(n, Equals, 2)
   349  }
   350  
   351  func (s *storeDownloadSuite) TestDownloadRangeRequestRetryOnHashError(c *C) {
   352  	expectedContentStr := "file was downloaded from scratch"
   353  	partialContentStr := "partial content "
   354  
   355  	n := 0
   356  	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 {
   357  		n++
   358  		if n == 1 {
   359  			// force sha3 error on first download
   360  			c.Check(resume, Equals, int64(len(partialContentStr)))
   361  			return store.NewHashError("foo", "1234", "5678")
   362  		}
   363  		w.Write([]byte(expectedContentStr))
   364  		return nil
   365  	})
   366  	defer restore()
   367  
   368  	snap := &snap.Info{}
   369  	snap.RealName = "foo"
   370  	snap.AnonDownloadURL = "anon-url"
   371  	snap.DownloadURL = "AUTH-URL"
   372  	snap.Sha3_384 = ""
   373  	snap.Size = int64(len(expectedContentStr))
   374  
   375  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   376  	err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
   377  	c.Assert(err, IsNil)
   378  
   379  	err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   380  	c.Assert(err, IsNil)
   381  	c.Assert(n, Equals, 2)
   382  
   383  	c.Assert(targetFn, testutil.FileEquals, expectedContentStr)
   384  }
   385  
   386  func (s *storeDownloadSuite) TestDownloadRangeRequestFailOnHashError(c *C) {
   387  	partialContentStr := "partial content "
   388  
   389  	n := 0
   390  	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 {
   391  		n++
   392  		return store.NewHashError("foo", "1234", "5678")
   393  	})
   394  	defer restore()
   395  
   396  	snap := &snap.Info{}
   397  	snap.RealName = "foo"
   398  	snap.AnonDownloadURL = "anon-url"
   399  	snap.DownloadURL = "AUTH-URL"
   400  	snap.Sha3_384 = ""
   401  	snap.Size = int64(len(partialContentStr) + 1)
   402  
   403  	targetFn := filepath.Join(c.MkDir(), "foo_1.0_all.snap")
   404  	err := ioutil.WriteFile(targetFn+".partial", []byte(partialContentStr), 0644)
   405  	c.Assert(err, IsNil)
   406  
   407  	err = s.store.Download(s.ctx, "foo", targetFn, &snap.DownloadInfo, nil, nil, nil)
   408  	c.Assert(err, NotNil)
   409  	c.Assert(err, ErrorMatches, `sha3-384 mismatch for "foo": got 1234 but expected 5678`)
   410  	c.Assert(n, Equals, 2)
   411  }
   412  
   413  func (s *storeDownloadSuite) TestAuthenticatedDownloadDoesNotUseAnonURL(c *C) {
   414  	expectedContent := []byte("I was downloaded")
   415  	restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, _ *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error {
   416  		// check user is pass and auth url is used
   417  		c.Check(user, Equals, s.user)
   418  		c.Check(url, Equals, "AUTH-URL")
   419  
   420  		w.Write(expectedContent)
   421  		return nil
   422  	})
   423  	defer restore()
   424  
   425  	snap := &snap.Info{}
   426  	snap.RealName = "foo"
   427  	snap.AnonDownloadURL = "anon-url"
   428  	snap.DownloadURL = "AUTH-URL"
   429  	snap.Size = int64(len(expectedContent))
   430  
   431  	path := filepath.Join(c.MkDir(), "downloaded-file")
   432  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, s.user, nil)
   433  	c.Assert(err, IsNil)
   434  	defer os.Remove(path)
   435  
   436  	c.Assert(path, testutil.FileEquals, expectedContent)
   437  }
   438  
   439  func (s *storeDownloadSuite) TestAuthenticatedDeviceDoesNotUseAnonURL(c *C) {
   440  	expectedContent := []byte("I was downloaded")
   441  	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 {
   442  		// check auth url is used
   443  		c.Check(url, Equals, "AUTH-URL")
   444  
   445  		w.Write(expectedContent)
   446  		return nil
   447  	})
   448  	defer restore()
   449  
   450  	snap := &snap.Info{}
   451  	snap.RealName = "foo"
   452  	snap.AnonDownloadURL = "anon-url"
   453  	snap.DownloadURL = "AUTH-URL"
   454  	snap.Size = int64(len(expectedContent))
   455  
   456  	dauthCtx := &testDauthContext{c: c, device: s.device}
   457  	sto := store.New(&store.Config{}, dauthCtx)
   458  
   459  	path := filepath.Join(c.MkDir(), "downloaded-file")
   460  	err := sto.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil)
   461  	c.Assert(err, IsNil)
   462  	defer os.Remove(path)
   463  
   464  	c.Assert(path, testutil.FileEquals, expectedContent)
   465  }
   466  
   467  func (s *storeDownloadSuite) TestLocalUserDownloadUsesAnonURL(c *C) {
   468  	expectedContentStr := "I was downloaded"
   469  	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 {
   470  		c.Check(url, Equals, "anon-url")
   471  
   472  		w.Write([]byte(expectedContentStr))
   473  		return nil
   474  	})
   475  	defer restore()
   476  
   477  	snap := &snap.Info{}
   478  	snap.RealName = "foo"
   479  	snap.AnonDownloadURL = "anon-url"
   480  	snap.DownloadURL = "AUTH-URL"
   481  	snap.Size = int64(len(expectedContentStr))
   482  
   483  	path := filepath.Join(c.MkDir(), "downloaded-file")
   484  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, s.localUser, nil)
   485  	c.Assert(err, IsNil)
   486  	defer os.Remove(path)
   487  
   488  	c.Assert(path, testutil.FileEquals, expectedContentStr)
   489  }
   490  
   491  func (s *storeDownloadSuite) TestDownloadFails(c *C) {
   492  	var tmpfile *os.File
   493  	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 {
   494  		tmpfile = w.(*os.File)
   495  		return fmt.Errorf("uh, it failed")
   496  	})
   497  	defer restore()
   498  
   499  	snap := &snap.Info{}
   500  	snap.RealName = "foo"
   501  	snap.AnonDownloadURL = "anon-url"
   502  	snap.DownloadURL = "AUTH-URL"
   503  	snap.Size = 1
   504  	// simulate a failed download
   505  	path := filepath.Join(c.MkDir(), "downloaded-file")
   506  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil)
   507  	c.Assert(err, ErrorMatches, "uh, it failed")
   508  	// ... and ensure that the tempfile is removed
   509  	c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
   510  	// ... and not because it succeeded either
   511  	c.Assert(osutil.FileExists(path), Equals, false)
   512  }
   513  
   514  func (s *storeDownloadSuite) TestDownloadFailsLeavePartial(c *C) {
   515  	var tmpfile *os.File
   516  	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 {
   517  		tmpfile = w.(*os.File)
   518  		w.Write([]byte{'X'}) // so it's not empty
   519  		return fmt.Errorf("uh, it failed")
   520  	})
   521  	defer restore()
   522  
   523  	snap := &snap.Info{}
   524  	snap.RealName = "foo"
   525  	snap.AnonDownloadURL = "anon-url"
   526  	snap.DownloadURL = "AUTH-URL"
   527  	snap.Size = 1
   528  	// simulate a failed download
   529  	path := filepath.Join(c.MkDir(), "downloaded-file")
   530  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, &store.DownloadOptions{LeavePartialOnError: true})
   531  	c.Assert(err, ErrorMatches, "uh, it failed")
   532  	// ... and ensure that the tempfile is *NOT* removed
   533  	c.Assert(osutil.FileExists(tmpfile.Name()), Equals, true)
   534  	// ... but the target path isn't there
   535  	c.Assert(osutil.FileExists(path), Equals, false)
   536  }
   537  
   538  func (s *storeDownloadSuite) TestDownloadFailsDoesNotLeavePartialIfEmpty(c *C) {
   539  	var tmpfile *os.File
   540  	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 {
   541  		tmpfile = w.(*os.File)
   542  		// no write, so the partial is empty
   543  		return fmt.Errorf("uh, it failed")
   544  	})
   545  	defer restore()
   546  
   547  	snap := &snap.Info{}
   548  	snap.RealName = "foo"
   549  	snap.AnonDownloadURL = "anon-url"
   550  	snap.DownloadURL = "AUTH-URL"
   551  	snap.Size = 1
   552  	// simulate a failed download
   553  	path := filepath.Join(c.MkDir(), "downloaded-file")
   554  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, &store.DownloadOptions{LeavePartialOnError: true})
   555  	c.Assert(err, ErrorMatches, "uh, it failed")
   556  	// ... and ensure that the tempfile *is* removed
   557  	c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
   558  	// ... and the target path isn't there
   559  	c.Assert(osutil.FileExists(path), Equals, false)
   560  }
   561  
   562  func (s *storeDownloadSuite) TestDownloadSyncFails(c *C) {
   563  	var tmpfile *os.File
   564  	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 {
   565  		tmpfile = w.(*os.File)
   566  		w.Write([]byte("sync will fail"))
   567  		err := tmpfile.Close()
   568  		c.Assert(err, IsNil)
   569  		return nil
   570  	})
   571  	defer restore()
   572  
   573  	snap := &snap.Info{}
   574  	snap.RealName = "foo"
   575  	snap.AnonDownloadURL = "anon-url"
   576  	snap.DownloadURL = "AUTH-URL"
   577  	snap.Size = int64(len("sync will fail"))
   578  
   579  	// simulate a failed sync
   580  	path := filepath.Join(c.MkDir(), "downloaded-file")
   581  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil)
   582  	c.Assert(err, ErrorMatches, `(sync|fsync:) .*`)
   583  	// ... and ensure that the tempfile is removed
   584  	c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
   585  	// ... because it's been renamed to the target path already
   586  	c.Assert(osutil.FileExists(path), Equals, true)
   587  }
   588  
   589  var downloadDeltaTests = []struct {
   590  	info          snap.DownloadInfo
   591  	authenticated bool
   592  	deviceSession bool
   593  	useLocalUser  bool
   594  	format        string
   595  	expectedURL   string
   596  	expectError   bool
   597  }{{
   598  	// An unauthenticated request downloads the anonymous delta url.
   599  	info: snap.DownloadInfo{
   600  		Sha3_384: "sha3",
   601  		Deltas: []snap.DeltaInfo{
   602  			{AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   603  		},
   604  	},
   605  	authenticated: false,
   606  	deviceSession: false,
   607  	format:        "xdelta3",
   608  	expectedURL:   "anon-delta-url",
   609  	expectError:   false,
   610  }, {
   611  	// An authenticated request downloads the authenticated delta url.
   612  	info: snap.DownloadInfo{
   613  		Sha3_384: "sha3",
   614  		Deltas: []snap.DeltaInfo{
   615  			{AnonDownloadURL: "anon-delta-url", DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   616  		},
   617  	},
   618  	authenticated: true,
   619  	deviceSession: false,
   620  	useLocalUser:  false,
   621  	format:        "xdelta3",
   622  	expectedURL:   "auth-delta-url",
   623  	expectError:   false,
   624  }, {
   625  	// A device-authenticated request downloads the authenticated delta url.
   626  	info: snap.DownloadInfo{
   627  		Sha3_384: "sha3",
   628  		Deltas: []snap.DeltaInfo{
   629  			{AnonDownloadURL: "anon-delta-url", DownloadURL: "auth-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   630  		},
   631  	},
   632  	authenticated: false,
   633  	deviceSession: true,
   634  	useLocalUser:  false,
   635  	format:        "xdelta3",
   636  	expectedURL:   "auth-delta-url",
   637  	expectError:   false,
   638  }, {
   639  	// A local authenticated request downloads the anonymous delta url.
   640  	info: snap.DownloadInfo{
   641  		Sha3_384: "sha3",
   642  		Deltas: []snap.DeltaInfo{
   643  			{AnonDownloadURL: "anon-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   644  		},
   645  	},
   646  	authenticated: true,
   647  	deviceSession: false,
   648  	useLocalUser:  true,
   649  	format:        "xdelta3",
   650  	expectedURL:   "anon-delta-url",
   651  	expectError:   false,
   652  }, {
   653  	// An error is returned if more than one matching delta is returned by the store,
   654  	// though this may be handled in the future.
   655  	info: snap.DownloadInfo{
   656  		Sha3_384: "sha3",
   657  		Deltas: []snap.DeltaInfo{
   658  			{DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 25},
   659  			{DownloadURL: "bsdiff-delta-url", Format: "xdelta3", FromRevision: 25, ToRevision: 26},
   660  		},
   661  	},
   662  	authenticated: false,
   663  	deviceSession: false,
   664  	format:        "xdelta3",
   665  	expectedURL:   "",
   666  	expectError:   true,
   667  }, {
   668  	// If the supported format isn't available, an error is returned.
   669  	info: snap.DownloadInfo{
   670  		Sha3_384: "sha3",
   671  		Deltas: []snap.DeltaInfo{
   672  			{DownloadURL: "xdelta3-delta-url", Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   673  			{DownloadURL: "ydelta-delta-url", Format: "ydelta", FromRevision: 24, ToRevision: 26},
   674  		},
   675  	},
   676  	authenticated: false,
   677  	deviceSession: false,
   678  	format:        "bsdiff",
   679  	expectedURL:   "",
   680  	expectError:   true,
   681  }}
   682  
   683  func (s *storeDownloadSuite) TestDownloadDelta(c *C) {
   684  	origUseDeltas := os.Getenv("SNAPD_USE_DELTAS_EXPERIMENTAL")
   685  	defer os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", origUseDeltas)
   686  	c.Assert(os.Setenv("SNAPD_USE_DELTAS_EXPERIMENTAL", "1"), IsNil)
   687  
   688  	dauthCtx := &testDauthContext{c: c}
   689  	sto := store.New(nil, dauthCtx)
   690  
   691  	for _, testCase := range downloadDeltaTests {
   692  		sto.SetDeltaFormat(testCase.format)
   693  		restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, _ *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error {
   694  			c.Check(dlOpts, DeepEquals, &store.DownloadOptions{IsAutoRefresh: true})
   695  			expectedUser := s.user
   696  			if testCase.useLocalUser {
   697  				expectedUser = s.localUser
   698  			}
   699  			if !testCase.authenticated {
   700  				expectedUser = nil
   701  			}
   702  			c.Check(user, Equals, expectedUser)
   703  			c.Check(url, Equals, testCase.expectedURL)
   704  			w.Write([]byte("I was downloaded"))
   705  			return nil
   706  		})
   707  		defer restore()
   708  
   709  		w, err := ioutil.TempFile("", "")
   710  		c.Assert(err, IsNil)
   711  		defer os.Remove(w.Name())
   712  
   713  		dauthCtx.device = nil
   714  		if testCase.deviceSession {
   715  			dauthCtx.device = s.device
   716  		}
   717  
   718  		authedUser := s.user
   719  		if testCase.useLocalUser {
   720  			authedUser = s.localUser
   721  		}
   722  		if !testCase.authenticated {
   723  			authedUser = nil
   724  		}
   725  
   726  		err = sto.DownloadDelta("snapname", &testCase.info, w, nil, authedUser, &store.DownloadOptions{IsAutoRefresh: true})
   727  
   728  		if testCase.expectError {
   729  			c.Assert(err, NotNil)
   730  		} else {
   731  			c.Assert(err, IsNil)
   732  			c.Assert(w.Name(), testutil.FileEquals, "I was downloaded")
   733  		}
   734  	}
   735  }
   736  
   737  var applyDeltaTests = []struct {
   738  	deltaInfo       snap.DeltaInfo
   739  	currentRevision uint
   740  	error           string
   741  }{{
   742  	// A supported delta format can be applied.
   743  	deltaInfo:       snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   744  	currentRevision: 24,
   745  	error:           "",
   746  }, {
   747  	// An error is returned if the expected current snap does not exist on disk.
   748  	deltaInfo:       snap.DeltaInfo{Format: "xdelta3", FromRevision: 24, ToRevision: 26},
   749  	currentRevision: 23,
   750  	error:           "snap \"foo\" revision 24 not found",
   751  }, {
   752  	// An error is returned if the format is not supported.
   753  	deltaInfo:       snap.DeltaInfo{Format: "nodelta", FromRevision: 24, ToRevision: 26},
   754  	currentRevision: 24,
   755  	error:           "cannot apply unsupported delta format \"nodelta\" (only xdelta3 currently)",
   756  }}
   757  
   758  func (s *storeDownloadSuite) TestApplyDelta(c *C) {
   759  	for _, testCase := range applyDeltaTests {
   760  		name := "foo"
   761  		currentSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.currentRevision)
   762  		currentSnapPath := filepath.Join(dirs.SnapBlobDir, currentSnapName)
   763  		targetSnapName := fmt.Sprintf("%s_%d.snap", name, testCase.deltaInfo.ToRevision)
   764  		targetSnapPath := filepath.Join(dirs.SnapBlobDir, targetSnapName)
   765  		err := os.MkdirAll(filepath.Dir(currentSnapPath), 0755)
   766  		c.Assert(err, IsNil)
   767  		err = ioutil.WriteFile(currentSnapPath, nil, 0644)
   768  		c.Assert(err, IsNil)
   769  		deltaPath := filepath.Join(dirs.SnapBlobDir, "the.delta")
   770  		err = ioutil.WriteFile(deltaPath, nil, 0644)
   771  		c.Assert(err, IsNil)
   772  		// When testing a case where the call to the external
   773  		// xdelta3 is successful,
   774  		// simulate the resulting .partial.
   775  		if testCase.error == "" {
   776  			err = ioutil.WriteFile(targetSnapPath+".partial", nil, 0644)
   777  			c.Assert(err, IsNil)
   778  		}
   779  
   780  		err = store.ApplyDelta(name, deltaPath, &testCase.deltaInfo, targetSnapPath, "")
   781  
   782  		if testCase.error == "" {
   783  			c.Assert(err, IsNil)
   784  			c.Assert(s.mockXDelta.Calls(), DeepEquals, [][]string{
   785  				{"xdelta3", "-d", "-s", currentSnapPath, deltaPath, targetSnapPath + ".partial"},
   786  			})
   787  			c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false)
   788  			st, err := os.Stat(targetSnapPath)
   789  			c.Assert(err, IsNil)
   790  			c.Check(st.Mode(), Equals, os.FileMode(0600))
   791  			c.Assert(os.Remove(targetSnapPath), IsNil)
   792  		} else {
   793  			c.Assert(err, NotNil)
   794  			c.Assert(err.Error()[0:len(testCase.error)], Equals, testCase.error)
   795  			c.Assert(osutil.FileExists(targetSnapPath+".partial"), Equals, false)
   796  			c.Assert(osutil.FileExists(targetSnapPath), Equals, false)
   797  		}
   798  		c.Assert(os.Remove(currentSnapPath), IsNil)
   799  		c.Assert(os.Remove(deltaPath), IsNil)
   800  	}
   801  }
   802  
   803  type cacheObserver struct {
   804  	inCache map[string]bool
   805  
   806  	gets []string
   807  	puts []string
   808  }
   809  
   810  func (co *cacheObserver) Get(cacheKey, targetPath string) error {
   811  	co.gets = append(co.gets, fmt.Sprintf("%s:%s", cacheKey, targetPath))
   812  	if !co.inCache[cacheKey] {
   813  		return fmt.Errorf("cannot find %s in cache", cacheKey)
   814  	}
   815  	return nil
   816  }
   817  func (co *cacheObserver) GetPath(cacheKey string) string {
   818  	return ""
   819  }
   820  func (co *cacheObserver) Put(cacheKey, sourcePath string) error {
   821  	co.puts = append(co.puts, fmt.Sprintf("%s:%s", cacheKey, sourcePath))
   822  	return nil
   823  }
   824  
   825  func (s *storeDownloadSuite) TestDownloadCacheHit(c *C) {
   826  	obs := &cacheObserver{inCache: map[string]bool{"the-snaps-sha3_384": true}}
   827  	restore := s.store.MockCacher(obs)
   828  	defer restore()
   829  
   830  	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 {
   831  		c.Fatalf("download should not be called when results come from the cache")
   832  		return nil
   833  	})
   834  	defer restore()
   835  
   836  	snap := &snap.Info{}
   837  	snap.Sha3_384 = "the-snaps-sha3_384"
   838  
   839  	path := filepath.Join(c.MkDir(), "downloaded-file")
   840  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil)
   841  	c.Assert(err, IsNil)
   842  
   843  	c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("%s:%s", snap.Sha3_384, path)})
   844  	c.Check(obs.puts, IsNil)
   845  }
   846  
   847  func (s *storeDownloadSuite) TestDownloadCacheMiss(c *C) {
   848  	obs := &cacheObserver{inCache: map[string]bool{}}
   849  	restore := s.store.MockCacher(obs)
   850  	defer restore()
   851  
   852  	downloadWasCalled := false
   853  	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 {
   854  		downloadWasCalled = true
   855  		return nil
   856  	})
   857  	defer restore()
   858  
   859  	snap := &snap.Info{}
   860  	snap.Sha3_384 = "the-snaps-sha3_384"
   861  
   862  	path := filepath.Join(c.MkDir(), "downloaded-file")
   863  	err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, nil)
   864  	c.Assert(err, IsNil)
   865  	c.Check(downloadWasCalled, Equals, true)
   866  
   867  	c.Check(obs.gets, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)})
   868  	c.Check(obs.puts, DeepEquals, []string{fmt.Sprintf("the-snaps-sha3_384:%s", path)})
   869  }
   870  
   871  func (s *storeDownloadSuite) TestDownloadStreamOK(c *C) {
   872  	expectedContent := []byte("I was downloaded")
   873  	restore := store.MockDoDownloadReq(func(ctx context.Context, url *url.URL, cdnHeader string, resume int64, s *store.Store, user *auth.UserState) (*http.Response, error) {
   874  		c.Check(url.String(), Equals, "http://anon-url")
   875  		r := &http.Response{
   876  			Body: ioutil.NopCloser(bytes.NewReader(expectedContent[resume:])),
   877  		}
   878  		if resume > 0 {
   879  			r.StatusCode = 206
   880  		} else {
   881  			r.StatusCode = 200
   882  		}
   883  		return r, nil
   884  	})
   885  	defer restore()
   886  
   887  	snap := &snap.Info{}
   888  	snap.RealName = "foo"
   889  	snap.AnonDownloadURL = "http://anon-url"
   890  	snap.DownloadURL = "AUTH-URL"
   891  	snap.Size = int64(len(expectedContent))
   892  
   893  	stream, status, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 0, nil)
   894  	c.Assert(err, IsNil)
   895  	c.Assert(status, Equals, 200)
   896  
   897  	buf := new(bytes.Buffer)
   898  	buf.ReadFrom(stream)
   899  	c.Check(buf.String(), Equals, string(expectedContent))
   900  
   901  	stream, status, err = s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 2, nil)
   902  	c.Assert(err, IsNil)
   903  	c.Check(status, Equals, 206)
   904  
   905  	buf = new(bytes.Buffer)
   906  	buf.ReadFrom(stream)
   907  	c.Check(buf.String(), Equals, string(expectedContent[2:]))
   908  }
   909  
   910  func (s *storeDownloadSuite) TestDownloadStreamCachedOK(c *C) {
   911  	expectedContent := []byte("I was NOT downloaded")
   912  	defer store.MockDoDownloadReq(func(context.Context, *url.URL, string, int64, *store.Store, *auth.UserState) (*http.Response, error) {
   913  		c.Fatalf("should not be here")
   914  		return nil, nil
   915  	})()
   916  
   917  	c.Assert(os.MkdirAll(dirs.SnapDownloadCacheDir, 0700), IsNil)
   918  	c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapDownloadCacheDir, "sha3_384-of-foo"), expectedContent, 0600), IsNil)
   919  
   920  	cache := store.NewCacheManager(dirs.SnapDownloadCacheDir, 1)
   921  	defer s.store.MockCacher(cache)()
   922  
   923  	snap := &snap.Info{}
   924  	snap.RealName = "foo"
   925  	snap.AnonDownloadURL = "http://anon-url"
   926  	snap.DownloadURL = "AUTH-URL"
   927  	snap.Size = int64(len(expectedContent))
   928  	snap.Sha3_384 = "sha3_384-of-foo"
   929  
   930  	stream, status, err := s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 0, nil)
   931  	c.Check(err, IsNil)
   932  	c.Check(status, Equals, 200)
   933  
   934  	buf := new(bytes.Buffer)
   935  	buf.ReadFrom(stream)
   936  	c.Check(buf.String(), Equals, string(expectedContent))
   937  
   938  	stream, status, err = s.store.DownloadStream(context.TODO(), "foo", &snap.DownloadInfo, 2, nil)
   939  	c.Assert(err, IsNil)
   940  	c.Check(status, Equals, 206)
   941  
   942  	buf = new(bytes.Buffer)
   943  	buf.ReadFrom(stream)
   944  	c.Check(buf.String(), Equals, string(expectedContent[2:]))
   945  }