github.com/pivotal-cf/go-pivnet/v6@v6.0.2/download/downloader_test.go (about)

     1  package download_test
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"net/url"
     9  	"strings"
    10  	"sync"
    11  
    12  	"github.com/pivotal-cf/go-pivnet/v6/download"
    13  	"github.com/pivotal-cf/go-pivnet/v6/download/fakes"
    14  
    15  	"fmt"
    16  	. "github.com/onsi/ginkgo"
    17  	. "github.com/onsi/gomega"
    18  	"net"
    19  	"syscall"
    20  	"math"
    21  	"time"
    22  	"github.com/pivotal-cf/go-pivnet/v6/logger/loggerfakes"
    23  	"os"
    24  )
    25  
    26  type EOFReader struct{}
    27  
    28  func (e EOFReader) Read(p []byte) (int, error) {
    29  	return 0, io.ErrUnexpectedEOF
    30  }
    31  
    32  type ConnectionResetReader struct{}
    33  
    34  func (e ConnectionResetReader) Read(p []byte) (int, error) {
    35  	return 0, &net.OpError{Err: fmt.Errorf(syscall.ECONNRESET.Error())}
    36  }
    37  
    38  type NetError struct {
    39  	error
    40  }
    41  
    42  func (ne NetError) Temporary() bool {
    43  	return true
    44  }
    45  
    46  func (ne NetError) Timeout() bool {
    47  	return true
    48  }
    49  
    50  type ReaderThatDoesntRead struct {}
    51  func (r ReaderThatDoesntRead) Read(p []byte) (int, error) {
    52  	for {
    53  		time.Sleep(time.Second)
    54  	}
    55  }
    56  
    57  var _ = Describe("Downloader", func() {
    58  	var (
    59  		httpClient          *fakes.HTTPClient
    60  		ranger              *fakes.Ranger
    61  		bar                 *fakes.Bar
    62  		downloadLinkFetcher *fakes.DownloadLinkFetcher
    63  	)
    64  
    65  	BeforeEach(func() {
    66  		httpClient = &fakes.HTTPClient{}
    67  		ranger = &fakes.Ranger{}
    68  		bar = &fakes.Bar{}
    69  
    70  		bar.NewProxyReaderStub = func(reader io.Reader) io.Reader { return reader }
    71  
    72  		downloadLinkFetcher = &fakes.DownloadLinkFetcher{}
    73  		downloadLinkFetcher.NewDownloadLinkStub = func() (string, error) {
    74  			return "https://example.com/some-file", nil
    75  		}
    76  	})
    77  
    78  	Describe("Get", func() {
    79  		It("writes the product to the given location", func() {
    80  			ranger.BuildRangeReturns([]download.Range{
    81  				download.NewRange(
    82  					0,
    83  					9,
    84  					http.Header{"Range": []string{"bytes=0-9"}},
    85  			),
    86  				download.NewRange(
    87  					10,
    88  					19,
    89  					http.Header{"Range": []string{"bytes=10-19"}},
    90  			),
    91  			}, nil)
    92  
    93  			var receivedRequests []*http.Request
    94  			var m = &sync.Mutex{}
    95  			httpClient.DoStub = func(req *http.Request) (*http.Response, error) {
    96  				m.Lock()
    97  				receivedRequests = append(receivedRequests, req)
    98  				m.Unlock()
    99  
   100  				switch req.Header.Get("Range") {
   101  				case "bytes=0-9":
   102  					return &http.Response{
   103  						StatusCode: http.StatusPartialContent,
   104  						Body:       ioutil.NopCloser(strings.NewReader("fake produ")),
   105  					}, nil
   106  				case "bytes=10-19":
   107  					return &http.Response{
   108  						StatusCode: http.StatusPartialContent,
   109  						Body:       ioutil.NopCloser(strings.NewReader("ct content")),
   110  					}, nil
   111  				default:
   112  					return &http.Response{
   113  						StatusCode:    http.StatusOK,
   114  						ContentLength: 10,
   115  						Request: &http.Request{
   116  							URL: &url.URL{
   117  								Scheme: "https",
   118  								Host:   "example.com",
   119  								Path:   "some-file",
   120  							},
   121  						},
   122  					}, nil
   123  				}
   124  			}
   125  
   126  			downloader := download.Client{
   127  				Logger:     &loggerfakes.FakeLogger{},
   128  				HTTPClient: httpClient,
   129  				Ranger:     ranger,
   130  				Bar:        bar,
   131  				Timeout:    5*time.Millisecond,
   132  			}
   133  
   134  			tmpFile, err := ioutil.TempFile("", "")
   135  			Expect(err).NotTo(HaveOccurred())
   136  
   137  			tmpLocation, err := download.NewFileInfo(tmpFile)
   138  			Expect(err).NotTo(HaveOccurred())
   139  
   140  			err = downloader.Get(tmpLocation, downloadLinkFetcher, GinkgoWriter)
   141  			Expect(err).NotTo(HaveOccurred())
   142  
   143  			content, err := ioutil.ReadAll(tmpFile)
   144  			Expect(err).NotTo(HaveOccurred())
   145  
   146  			Expect(string(content)).To(Equal("fake product content"))
   147  
   148  			Expect(ranger.BuildRangeCallCount()).To(Equal(1))
   149  			Expect(ranger.BuildRangeArgsForCall(0)).To(Equal(int64(10)))
   150  
   151  			Expect(bar.SetTotalArgsForCall(0)).To(Equal(int64(10)))
   152  			Expect(bar.KickoffCallCount()).To(Equal(1))
   153  
   154  			Expect(httpClient.DoCallCount()).To(Equal(3))
   155  
   156  			methods := []string{
   157  				receivedRequests[0].Method,
   158  				receivedRequests[1].Method,
   159  				receivedRequests[2].Method,
   160  			}
   161  			urls := []string{
   162  				receivedRequests[0].URL.String(),
   163  				receivedRequests[1].URL.String(),
   164  				receivedRequests[2].URL.String(),
   165  			}
   166  			headers := []string{
   167  				receivedRequests[1].Header.Get("Range"),
   168  				receivedRequests[2].Header.Get("Range"),
   169  			}
   170  			refererHeaders := []string {
   171  				receivedRequests[0].Header.Get("Referer"),
   172  				receivedRequests[1].Header.Get("Referer"),
   173  				receivedRequests[2].Header.Get("Referer"),
   174  			}
   175  
   176  			Expect(methods).To(ConsistOf([]string{"HEAD", "GET", "GET"}))
   177  			Expect(urls).To(ConsistOf([]string{"https://example.com/some-file", "https://example.com/some-file", "https://example.com/some-file"}))
   178  			Expect(headers).To(ConsistOf([]string{"bytes=0-9", "bytes=10-19"}))
   179  			Expect(refererHeaders).To(ConsistOf([]string{
   180  				"https://go-pivnet.network.pivotal.io",
   181  				"https://go-pivnet.network.pivotal.io",
   182  				"https://go-pivnet.network.pivotal.io",
   183  				}))
   184  
   185  			Expect(bar.FinishCallCount()).To(Equal(1))
   186  		})
   187  	})
   188  
   189  	Context("when a retryable error occurs", func() {
   190  		var (
   191  			responses             []*http.Response
   192  			responseErrors        []error
   193  			tmpFile               *os.File
   194  		)
   195  
   196  		JustBeforeEach(func() {
   197  			defaultErrors := []error{nil}
   198  			defaultResponses := []*http.Response{
   199  				{
   200  					Request: &http.Request{
   201  						URL: &url.URL{
   202  							Scheme: "https",
   203  							Host:   "example.com",
   204  							Path:   "some-file",
   205  						},
   206  					},
   207  				},
   208  			}
   209  
   210  			responseErrors = append(defaultErrors, responseErrors...)
   211  			responses = append(defaultResponses, responses...)
   212  
   213  			httpClient.DoStub = func(req *http.Request) (*http.Response, error) {
   214  				count := httpClient.DoCallCount() - 1
   215  				return responses[count], responseErrors[count]
   216  			}
   217  
   218  			ranger.BuildRangeReturns([]download.Range{download.NewRange(0,15, http.Header{})}, nil)
   219  
   220  			downloader := download.Client{
   221  				Logger:     &loggerfakes.FakeLogger{},
   222  				HTTPClient: httpClient,
   223  				Ranger:     ranger,
   224  				Bar:        bar,
   225  				Timeout: 	5*time.Millisecond,
   226  			}
   227  
   228  			var err error
   229  			tmpFile, err = ioutil.TempFile("", "")
   230  			Expect(err).NotTo(HaveOccurred())
   231  
   232  			tmpLocation, err := download.NewFileInfo(tmpFile)
   233  			Expect(err).NotTo(HaveOccurred())
   234  
   235  			err = downloader.Get(tmpLocation, downloadLinkFetcher, GinkgoWriter)
   236  			Expect(err).NotTo(HaveOccurred())
   237  		})
   238  
   239  		Context("when there is an unexpected EOF", func() {
   240  			BeforeEach(func() {
   241  				responseErrors = []error{nil, nil}
   242  				responses = []*http.Response{
   243  					{
   244  						StatusCode: http.StatusPartialContent,
   245  						Body:       ioutil.NopCloser(io.MultiReader(strings.NewReader("some"), EOFReader{})),
   246  					},
   247  					{
   248  						StatusCode: http.StatusPartialContent,
   249  						Body:       ioutil.NopCloser(strings.NewReader("something")),
   250  					},
   251  				}
   252  			})
   253  
   254  			It("successfully retries the download", func() {
   255  				stats, err := tmpFile.Stat()
   256  				Expect(err).NotTo(HaveOccurred())
   257  
   258  				Expect(stats.Size()).To(BeNumerically(">", 0))
   259  
   260  				Expect(bar.AddArgsForCall(0)).To(Equal(-4))
   261  
   262  				content, err := ioutil.ReadAll(tmpFile)
   263  				Expect(err).NotTo(HaveOccurred())
   264  
   265  				Expect(string(content)).To(Equal("something"))
   266  			})
   267  		})
   268  
   269  		Context("when there is a temporary network error", func() {
   270  			BeforeEach(func() {
   271  				responses = []*http.Response{
   272  					{
   273  						StatusCode: http.StatusPartialContent,
   274  					},
   275  					{
   276  						StatusCode: http.StatusPartialContent,
   277  						Body:       ioutil.NopCloser(strings.NewReader("something")),
   278  					},
   279  				}
   280  				responseErrors = []error{NetError{errors.New("whoops")}, nil}
   281  			})
   282  
   283  			It("successfully retries the download", func() {
   284  				stats, err := tmpFile.Stat()
   285  				Expect(err).NotTo(HaveOccurred())
   286  
   287  				Expect(stats.Size()).To(BeNumerically(">", 0))
   288  			})
   289  		})
   290  
   291  		Context("when the connection is reset", func() {
   292  			BeforeEach(func() {
   293  				responses = []*http.Response{
   294  					{
   295  						StatusCode: http.StatusPartialContent,
   296  						Body:       ioutil.NopCloser(io.MultiReader(strings.NewReader("some"), ConnectionResetReader{})),
   297  					},
   298  					{
   299  						StatusCode: http.StatusPartialContent,
   300  						Body:       ioutil.NopCloser(strings.NewReader("something")),
   301  					},
   302  				}
   303  
   304  				responseErrors = []error{nil, nil}
   305  
   306  			})
   307  
   308  			It("successfully retries the download", func() {
   309  				stats, err := tmpFile.Stat()
   310  				Expect(err).NotTo(HaveOccurred())
   311  
   312  				Expect(stats.Size()).To(BeNumerically(">", 0))
   313  				Expect(bar.AddArgsForCall(0)).To(Equal(-4))
   314  
   315  				content, err := ioutil.ReadAll(tmpFile)
   316  				Expect(err).NotTo(HaveOccurred())
   317  
   318  				Expect(string(content)).To(Equal("something"))
   319  			})
   320  		})
   321  
   322  		Context("when there is a timeout", func() {
   323  			BeforeEach(func() {
   324  				responses = []*http.Response{
   325  					{
   326  						StatusCode: http.StatusPartialContent,
   327  						Body:       ioutil.NopCloser(io.MultiReader(strings.NewReader("some"), ReaderThatDoesntRead{})),
   328  					},
   329  					{
   330  						StatusCode: http.StatusPartialContent,
   331  						Body:       ioutil.NopCloser(strings.NewReader("something")),
   332  					},
   333  				}
   334  
   335  				responseErrors = []error{nil, nil}
   336  
   337  			})
   338  
   339  			It("retries", func() {
   340  				stats, err := tmpFile.Stat()
   341  				Expect(err).NotTo(HaveOccurred())
   342  
   343  				Expect(stats.Size()).To(BeNumerically(">", 0))
   344  				Expect(bar.AddArgsForCall(0)).To(Equal(-4))
   345  
   346  				content, err := ioutil.ReadAll(tmpFile)
   347  				Expect(err).NotTo(HaveOccurred())
   348  
   349  				Expect(string(content)).To(Equal("something"))
   350  			})
   351  		})
   352  	})
   353  
   354  	Context("when an error occurs", func() {
   355  		Context("when the disk is out of memory", func() {
   356  			It("returns an error", func() {
   357  				tooBig := int64(math.MaxInt64)
   358  				responses := []*http.Response{
   359  					{
   360  						Request: &http.Request{
   361  							URL: &url.URL{
   362  								Scheme: "https",
   363  								Host:   "example.com",
   364  								Path:   "some-file",
   365  							},
   366  						},
   367  						ContentLength: tooBig,
   368  					},
   369  				}
   370  				errors := []error{nil, nil}
   371  
   372  				httpClient.DoStub = func(req *http.Request) (*http.Response, error) {
   373  					count := httpClient.DoCallCount() - 1
   374  					return responses[count], errors[count]
   375  				}
   376  
   377  
   378  				downloader := download.Client{
   379  					Logger:     &loggerfakes.FakeLogger{},
   380  					HTTPClient: httpClient,
   381  					Ranger:     ranger,
   382  					Bar:        bar,
   383  					Timeout: 	5 * time.Millisecond,
   384  				}
   385  
   386  				file, err := ioutil.TempFile("", "")
   387  				Expect(err).NotTo(HaveOccurred())
   388  
   389  				location, err := download.NewFileInfo(file)
   390  				Expect(err).NotTo(HaveOccurred())
   391  
   392  				err = downloader.Get(location, downloadLinkFetcher, GinkgoWriter)
   393  				Expect(err).To(MatchError(ContainSubstring("file is too big to fit on this drive:")))
   394  				Expect(err).To(MatchError(ContainSubstring("bytes required")))
   395  				Expect(err).To(MatchError(ContainSubstring("bytes free")))
   396  			})
   397  		})
   398  
   399  		Context("when content length is -1", func() {
   400  			It("returns an error", func() {
   401  				invalidLength := int64(-1)
   402  
   403  				responses := []*http.Response{
   404  					{
   405  						Request: &http.Request{
   406  							URL: &url.URL{
   407  								Scheme: "https",
   408  								Host:   "example.com",
   409  								Path:   "some-file",
   410  							},
   411  						},
   412  						ContentLength: invalidLength,
   413  					},
   414  				}
   415  				errors := []error{nil, nil}
   416  
   417  				httpClient.DoStub = func(req *http.Request) (*http.Response, error) {
   418  					count := httpClient.DoCallCount() - 1
   419  					return responses[count], errors[count]
   420  				}
   421  
   422  
   423  				downloader := download.Client{
   424  					Logger:     &loggerfakes.FakeLogger{},
   425  					HTTPClient: httpClient,
   426  					Ranger:     ranger,
   427  					Bar:        bar,
   428  					Timeout: 	5 * time.Millisecond,
   429  				}
   430  
   431  				file, err := ioutil.TempFile("", "")
   432  				Expect(err).NotTo(HaveOccurred())
   433  
   434  				location, err := download.NewFileInfo(file)
   435  				Expect(err).NotTo(HaveOccurred())
   436  
   437  				err = downloader.Get(location, downloadLinkFetcher, GinkgoWriter)
   438  				Expect(err).To(MatchError(ContainSubstring("failed to find file")))
   439  			})
   440  		})
   441  
   442  		Context("when the HEAD request cannot be constucted", func() {
   443  			It("returns an error", func() {
   444  				downloader := download.Client{
   445  					Logger:     &loggerfakes.FakeLogger{},
   446  					HTTPClient: nil,
   447  					Ranger:     nil,
   448  					Bar:        nil,
   449  					Timeout: 	5 * time.Millisecond,
   450  				}
   451  				downloadLinkFetcher.NewDownloadLinkStub = func() (string, error) {
   452  					return "%%%", nil
   453  				}
   454  
   455  				err := downloader.Get(nil, downloadLinkFetcher, GinkgoWriter)
   456  				Expect(err).To(MatchError(ContainSubstring("failed to construct HEAD request")))
   457  			})
   458  		})
   459  
   460  		Context("when the HEAD has an error", func() {
   461  			It("returns an error", func() {
   462  				httpClient.DoReturns(&http.Response{}, errors.New("failed request"))
   463  
   464  				downloader := download.Client{
   465  					Logger:     &loggerfakes.FakeLogger{},
   466  					HTTPClient: httpClient,
   467  					Ranger:     nil,
   468  					Bar:        nil,
   469  					Timeout: 	5 * time.Millisecond,
   470  				}
   471  
   472  				err := downloader.Get(nil, downloadLinkFetcher, GinkgoWriter)
   473  				Expect(err).To(MatchError("failed to make HEAD request: failed request"))
   474  			})
   475  		})
   476  
   477  		Context("when building a range fails", func() {
   478  			It("returns an error", func() {
   479  				httpClient.DoReturns(&http.Response{Request: &http.Request{
   480  					URL: &url.URL{
   481  						Scheme: "https",
   482  						Host:   "example.com",
   483  						Path:   "some-file",
   484  					},
   485  				},
   486  				}, nil)
   487  
   488  				ranger.BuildRangeReturns([]download.Range{}, errors.New("failed range build"))
   489  
   490  				downloader := download.Client{
   491  					Logger:     &loggerfakes.FakeLogger{},
   492  					HTTPClient: httpClient,
   493  					Ranger:     ranger,
   494  					Bar:        nil,
   495  					Timeout: 	5 * time.Millisecond,
   496  				}
   497  
   498  				err := downloader.Get(nil, downloadLinkFetcher, GinkgoWriter)
   499  				Expect(err).To(MatchError("failed to construct range: failed range build"))
   500  			})
   501  		})
   502  
   503  		Context("when the GET fails", func() {
   504  			It("returns an error", func() {
   505  				responses := []*http.Response{
   506  					{
   507  						Request: &http.Request{
   508  							URL: &url.URL{
   509  								Scheme: "https",
   510  								Host:   "example.com",
   511  								Path:   "some-file",
   512  							},
   513  						},
   514  					},
   515  					{},
   516  				}
   517  				errors := []error{nil, errors.New("failed GET")}
   518  
   519  				httpClient.DoStub = func(req *http.Request) (*http.Response, error) {
   520  					count := httpClient.DoCallCount() - 1
   521  					return responses[count], errors[count]
   522  				}
   523  
   524  				ranger.BuildRangeReturns([]download.Range{download.NewRange(0, 0, http.Header{})}, nil)
   525  
   526  				downloader := download.Client{
   527  					Logger:     &loggerfakes.FakeLogger{},
   528  					HTTPClient: httpClient,
   529  					Ranger:     ranger,
   530  					Bar:        bar,
   531  					Timeout: 	5 * time.Millisecond,
   532  				}
   533  
   534  				file, err := ioutil.TempFile("", "")
   535  				Expect(err).NotTo(HaveOccurred())
   536  
   537  				location, err := download.NewFileInfo(file)
   538  				Expect(err).NotTo(HaveOccurred())
   539  
   540  				err = downloader.Get(location, downloadLinkFetcher, GinkgoWriter)
   541  				Expect(err).To(MatchError("problem while waiting for chunks to download: failed during retryable request: download request failed: failed GET"))
   542  			})
   543  		})
   544  
   545  		Context("when the GET returns a non-206", func() {
   546  			It("returns an error", func() {
   547  				responses := []*http.Response{
   548  					{
   549  						Request: &http.Request{
   550  							URL: &url.URL{
   551  								Scheme: "https",
   552  								Host:   "example.com",
   553  								Path:   "some-file",
   554  							},
   555  						},
   556  					},
   557  					{
   558  						StatusCode: http.StatusInternalServerError,
   559  						Body:       ioutil.NopCloser(strings.NewReader("")),
   560  					},
   561  				}
   562  				errors := []error{nil, nil}
   563  
   564  				httpClient.DoStub = func(req *http.Request) (*http.Response, error) {
   565  					count := httpClient.DoCallCount() - 1
   566  					return responses[count], errors[count]
   567  				}
   568  
   569  				ranger.BuildRangeReturns([]download.Range{download.NewRange(0, 0, http.Header{})}, nil)
   570  
   571  				downloader := download.Client{
   572  					Logger:     &loggerfakes.FakeLogger{},
   573  					HTTPClient: httpClient,
   574  					Ranger:     ranger,
   575  					Bar:        bar,
   576  					Timeout: 	5 * time.Millisecond,
   577  				}
   578  
   579  				file, err := ioutil.TempFile("", "")
   580  				Expect(err).NotTo(HaveOccurred())
   581  
   582  				location, err := download.NewFileInfo(file)
   583  				Expect(err).NotTo(HaveOccurred())
   584  
   585  				err = downloader.Get(location, downloadLinkFetcher, GinkgoWriter)
   586  				Expect(err).To(MatchError("problem while waiting for chunks to download: failed during retryable request: during GET unexpected status code was returned: 500"))
   587  			})
   588  		})
   589  	})
   590  })