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 })