blob: d3a83a25fa394d674b84c10ea6325e99ae7061fc [file] [edit]
package httputil
import (
"net/http"
"strconv"
"testing"
"time"
)
var (
seed = time.Now()
)
type fakeClock struct {
now time.Time
SleepPeriods []time.Duration
}
func newFakeClock() *fakeClock {
return &fakeClock{now: seed}
}
func (fc *fakeClock) Sleep(d time.Duration) {
fc.now = fc.now.Add(d)
fc.SleepPeriods = append(fc.SleepPeriods, d)
}
func (fc *fakeClock) Now() time.Time {
return fc.now
}
func (fc *fakeClock) TimesSlept() int {
return len(fc.SleepPeriods)
}
func setUp() (*FakeTransport, *fakeClock) {
transport := NewFakeTransport()
DefaultTransport = transport
clock := newFakeClock()
RetryClock = clock
return transport, clock
}
func setUpAllFailures(url string, status, retries int, headers map[string]string) (*FakeTransport, *fakeClock) {
MaxRetries = retries
transport, clock := setUp()
for i := 0; i <= retries; i++ {
transport.AddResponse(url, status, "", headers)
}
return transport, clock
}
func TestSuccessOnFirstTry(t *testing.T) {
transport, _ := setUp()
url := "http://foo"
want := "the_body"
transport.AddResponse(url, 200, want, nil)
body, _, err := ReadRemoteFile(url, "")
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
got := string(body)
if got != want {
t.Fatalf("Expected body %q, but got %q", want, got)
}
}
func TestSuccessOnRetry(t *testing.T) {
transport, clock := setUp()
url := "http://foo"
want := "the_body"
transport.AddResponse(url, 503, "", nil)
transport.AddResponse(url, 200, want, nil)
body, _, err := ReadRemoteFile(url, "")
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
got := string(body)
if got != want {
t.Fatalf("Expected body %q, but got %q", want, got)
}
if clock.TimesSlept() != 1 {
t.Fatalf("Expected a single retry, not %d", clock.TimesSlept())
}
}
func TestAllTriesFail(t *testing.T) {
MaxRequestDuration = 100 * time.Second
url := "http://bar"
retries := 5
_, clock := setUpAllFailures(url, 502, retries, nil)
_, _, err := ReadRemoteFile(url, "")
if err == nil {
t.Fatal("Expected request to fail with code 502")
}
reason := err.Error()
expected := "could not fetch http://bar: unable to complete request to http://bar after 5 retries. Most recent status: 502"
if reason != expected {
t.Fatalf("Expected request to fail with %q, but got %q", expected, reason)
}
if clock.TimesSlept() != retries {
t.Fatalf("Expected %d retries, but got %d", retries, clock.TimesSlept())
}
}
func TestShouldObeyRetryHeaders(t *testing.T) {
MaxRequestDuration = time.Hour * 24
headerGens := []func(time.Duration) string{
func(d time.Duration) string {
// Value == earliest date to retry
return seed.Add(d).UTC().Format(http.TimeFormat)
},
func(d time.Duration) string {
// Value == seconds to wait
return strconv.Itoa(int(d.Seconds()))
},
}
for _, header := range retryHeaders {
for _, gen := range headerGens {
url := "http://bar"
wanted := 5 * time.Hour
_, clock := setUpAllFailures(url, 501, 1, map[string]string{header: gen(wanted)})
_, _, err := ReadRemoteFile(url, "")
if err == nil {
t.Fatal("Expected request to fail with code 502")
}
if clock.TimesSlept() != 1 {
t.Fatalf("Expected a single retry, but got %d", clock.TimesSlept())
}
got := clock.SleepPeriods[0]
delta := time.Minute
if got < wanted-delta || wanted+delta < got {
t.Errorf("Expected a retry after roughly %s, but actually waited for %s", wanted, got)
}
}
}
}
func TestShouldUseExponentialBackoffIfNoRetryHeader(t *testing.T) {
MaxRequestDuration = time.Hour * 24
url := "http://bar"
retries := 5
_, clock := setUpAllFailures(url, 501, retries, nil)
_, _, err := ReadRemoteFile(url, "")
if err == nil {
t.Fatal("Expected request to fail with code 501")
}
if clock.TimesSlept() != retries {
t.Fatalf("Expected %d retries, but got %d", retries, clock.TimesSlept())
}
var total time.Duration
for _, p := range clock.SleepPeriods {
total += p
}
delta := time.Millisecond * 100
lower := (1 << uint(retries)) * time.Second
upper := lower + time.Duration(retries*500)*time.Millisecond
if total < lower-delta || upper+delta < total {
t.Fatalf("Expected a total sleep time between %s and %s (%d retries, exponential backoff with fuzzing), but waited %s instead", lower, upper, retries, total)
}
}
func TestDeadlineExceeded(t *testing.T) {
MaxRequestDuration = time.Second * 8
url := "http://bar"
setUpAllFailures(url, 500, 10, nil)
_, _, err := ReadRemoteFile(url, "")
if err == nil {
t.Fatal("Expected request to fail with code 500")
}
wanted := "could not fetch http://bar: unable to complete request to http://bar within 8s"
got := err.Error()
if wanted != got {
t.Fatalf("Expected error %q, but got %q", wanted, got)
}
}
func TestNoRetryOnPermanentError(t *testing.T) {
MaxRequestDuration = time.Hour
url := "http://xyz"
_, clock := setUpAllFailures(url, 404, 3, nil)
_, _, err := ReadRemoteFile(url, "")
if err == nil {
t.Fatal("Expected request to fail with code 404")
}
wanted := "unexpected status code while reading http://xyz: 404"
got := err.Error()
if wanted != got {
t.Fatalf("Expected error %q, but got %q", wanted, got)
}
if clock.TimesSlept() > 0 {
t.Fatalf("Expected no retries for permanent error, but got %d", clock.TimesSlept())
}
}