package httpx import ( "context" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" ) func noSleep(context.Context, time.Duration) error { return nil } func TestRetrierRetriesOn5xxAnd429(t *testing.T) { var calls int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch atomic.AddInt32(&calls, 1) { case 1: w.WriteHeader(http.StatusTooManyRequests) case 2: w.WriteHeader(http.StatusBadGateway) default: w.WriteHeader(http.StatusOK) } })) defer srv.Close() r := Retrier{MaxAttempts: 4, BaseDelay: time.Millisecond, Sleep: noSleep} resp, err := r.Do(context.Background(), srv.Client(), func() (*http.Request, error) { return http.NewRequest(http.MethodGet, srv.URL, nil) }) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 || calls != 3 { t.Fatalf("status=%d calls=%d", resp.StatusCode, calls) } } func TestRetrierNoRetryOn4xx(t *testing.T) { var calls int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt32(&calls, 1) w.WriteHeader(http.StatusNotFound) })) defer srv.Close() r := Retrier{MaxAttempts: 4, BaseDelay: time.Millisecond, Sleep: noSleep} resp, err := r.Do(context.Background(), srv.Client(), func() (*http.Request, error) { return http.NewRequest(http.MethodGet, srv.URL, nil) }) if err != nil { t.Fatal(err) } resp.Body.Close() if calls != 1 || resp.StatusCode != 404 { t.Fatalf("calls=%d status=%d", calls, resp.StatusCode) } } func TestRetrierExhaustedReturnsLastResponse(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) })) defer srv.Close() r := Retrier{MaxAttempts: 2, BaseDelay: time.Millisecond, Sleep: noSleep} resp, err := r.Do(context.Background(), srv.Client(), func() (*http.Request, error) { return http.NewRequest(http.MethodGet, srv.URL, nil) }) if err != nil { t.Fatal(err) } resp.Body.Close() if resp.StatusCode != 503 { t.Fatalf("status=%d", resp.StatusCode) } } func TestRetrierNetworkError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) url := srv.URL srv.Close() r := Retrier{MaxAttempts: 3, BaseDelay: time.Millisecond, Sleep: noSleep} _, err := r.Do(context.Background(), http.DefaultClient, func() (*http.Request, error) { return http.NewRequest(http.MethodGet, url, nil) }) if err == nil { t.Fatal("ожидалась ошибка") } } func TestBackoffCapped(t *testing.T) { r := Retrier{BaseDelay: time.Second, MaxDelay: 5 * time.Second} if got := r.backoff(1); got != time.Second { t.Errorf("attempt1 = %v", got) } if got := r.backoff(10); got != 5*time.Second { t.Errorf("attempt10 = %v", got) } }