// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package api import ( "context" "encoding/json" "math" "net/http" "net/http/httptest" "testing" "time" "github.com/shoenig/test/must" ) type mockHandler struct { callsCounter []time.Time } func (mh *mockHandler) Handle(rw http.ResponseWriter, req *http.Request) { mh.callsCounter = append(mh.callsCounter, time.Now()) // return a populated meta after 7 tries to test he retries stops after a // successful call if len(mh.callsCounter) < 7 { http.Error(rw, http.StatusText(http.StatusBadGateway), http.StatusBadGateway) return } rw.WriteHeader(http.StatusOK) rw.Header().Set("Content-Type", "application/json") resp := &WriteMeta{} jsonResp, _ := json.Marshal(resp) rw.Write(jsonResp) return } func Test_RetryPut_multiple_calls(t *testing.T) { t.Run("successfully retries until no error, delayed capped to 100ms", func(t *testing.T) { mh := mockHandler{ callsCounter: []time.Time{}, } server := httptest.NewServer(http.HandlerFunc(mh.Handle)) cm, err := NewClient(&Config{ Address: server.URL, retryOptions: &retryOptions{ delayBase: 10 * time.Millisecond, maxRetries: 10, maxBackoffDelay: 100 * time.Millisecond, }, }) must.NoError(t, err) md, err := cm.retryPut(context.TODO(), "/endpoint", nil, nil, &WriteOptions{}) must.NoError(t, err) must.Len(t, 7, mh.callsCounter) must.NotNil(t, md) must.Greater(t, 10*time.Millisecond, mh.callsCounter[1].Sub(mh.callsCounter[0])) must.Greater(t, 20*time.Millisecond, mh.callsCounter[2].Sub(mh.callsCounter[1])) must.Greater(t, 40*time.Millisecond, mh.callsCounter[3].Sub(mh.callsCounter[2])) must.Greater(t, 80*time.Millisecond, mh.callsCounter[4].Sub(mh.callsCounter[3])) must.Greater(t, 100*time.Millisecond, mh.callsCounter[5].Sub(mh.callsCounter[4])) must.Greater(t, 100*time.Millisecond, mh.callsCounter[6].Sub(mh.callsCounter[5])) }) } func Test_RetryPut_one_call(t *testing.T) { t.Run("successfully retries until no error, delayed capped to 100ms", func(t *testing.T) { mh := mockHandler{ callsCounter: []time.Time{}, } server := httptest.NewServer(http.HandlerFunc(mh.Handle)) cm, err := NewClient(&Config{ Address: server.URL, retryOptions: &retryOptions{ delayBase: 10 * time.Millisecond, maxRetries: 1, }, }) must.NoError(t, err) md, err := cm.retryPut(context.TODO(), "/endpoint/", nil, nil, &WriteOptions{}) must.Error(t, err) must.Nil(t, md) must.Len(t, 2, mh.callsCounter) }) } func Test_RetryPut_capped_base_too_big(t *testing.T) { t.Run("successfully retries until no error, delayed capped to 100ms", func(t *testing.T) { mh := mockHandler{ callsCounter: []time.Time{}, } server := httptest.NewServer(http.HandlerFunc(mh.Handle)) cm, err := NewClient(&Config{ Address: server.URL, retryOptions: &retryOptions{ delayBase: math.MaxInt64 * time.Nanosecond, maxRetries: 3, maxBackoffDelay: 200 * time.Millisecond, }, }) must.NoError(t, err) md, err := cm.retryPut(context.TODO(), "/endpoint", nil, nil, &WriteOptions{}) must.Error(t, err) must.Len(t, 4, mh.callsCounter) must.Nil(t, md) must.Greater(t, cm.config.retryOptions.maxBackoffDelay, mh.callsCounter[1].Sub(mh.callsCounter[0])) must.Greater(t, cm.config.retryOptions.maxBackoffDelay, mh.callsCounter[2].Sub(mh.callsCounter[1])) }) }