diff --git a/pkg/api/v1/client_test.go b/pkg/api/v1/client_test.go index 73db215..ac91ad8 100644 --- a/pkg/api/v1/client_test.go +++ b/pkg/api/v1/client_test.go @@ -4,8 +4,11 @@ import ( "context" "net/http" "net/http/httptest" + "sync/atomic" "testing" + "time" + "github.com/cenkalti/backoff/v4" "github.com/stretchr/testify/require" "github.com/trisacrypto/courier/pkg/api/v1" ) @@ -63,3 +66,31 @@ func TestStoreCertificatePassword(t *testing.T) { err = client.StoreCertificatePassword(context.Background(), req) require.ErrorIs(t, err, api.ErrIDRequired, "client should error if no ID is provided") } + +func TestRetriesWithBackoff(t *testing.T) { + // Create a test server + var attempts uint32 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddUint32(&attempts, 1) + http.Error(w, http.StatusText(http.StatusTooEarly), http.StatusTooEarly) + })) + defer ts.Close() + + // Create a client to test the client method + client, err := api.New(ts.URL, api.WithRetries(10), api.WithBackoff(func() backoff.BackOff { + return backoff.NewConstantBackOff(100 * time.Millisecond) + })) + require.NoError(t, err, "could not create client") + + rawClient, ok := client.(*api.APIv1) + require.True(t, ok, "expected client to be an APIv1 client") + + req, err := rawClient.NewRequest(context.Background(), http.MethodGet, "/", nil, nil) + require.NoError(t, err, "could not create request") + + start := time.Now() + _, err = rawClient.Do(req, nil, true) + require.Error(t, err, "expected an error to be returned") + require.Equal(t, uint32(11), attempts, "expected 10 retry attempts") + require.Greater(t, time.Since(start), 950*time.Millisecond, "expected backoff delay") +} diff --git a/pkg/api/v1/errors.go b/pkg/api/v1/errors.go index 20b7d28..50b374e 100644 --- a/pkg/api/v1/errors.go +++ b/pkg/api/v1/errors.go @@ -47,6 +47,9 @@ func ErrorResponse(err interface{}) Reply { } func NewStatusError(code int, err string) error { + if err == "" { + err = http.StatusText(code) + } return &StatusError{Code: code, Err: err} } diff --git a/pkg/api/v1/errors_test.go b/pkg/api/v1/errors_test.go index 8b97634..cf5e761 100644 --- a/pkg/api/v1/errors_test.go +++ b/pkg/api/v1/errors_test.go @@ -20,12 +20,12 @@ func TestJoinStatusErrors(t *testing.T) { }) t.Run("SingleStatusError", func(t *testing.T) { - err := api.JoinStatusErrors(1, 421*time.Millisecond, api.NewStatusError(http.StatusServiceUnavailable, "could not reach specified service")) + err := api.JoinStatusErrors(1, 421*time.Millisecond, api.NewStatusError(http.StatusServiceUnavailable, "")) require.Error(t, err, "expected error to be returned") - serr, ok := err.(*api.StatusError) + _, ok := err.(*api.StatusError) require.True(t, ok, "expected error to be a status error, not a multi status error") - require.Equal(t, 503, serr.Code) + require.EqualError(t, err, "[503]: Service Unavailable") }) t.Run("SingleError", func(t *testing.T) { @@ -37,15 +37,69 @@ func TestJoinStatusErrors(t *testing.T) { require.EqualError(t, err, "something went wrong") }) - t.Run("MultiStatusErrors", func(t *testing.T) {}) + t.Run("MultiStatusErrors", func(t *testing.T) { + err := api.JoinStatusErrors(3, 1829*time.Millisecond, + api.NewStatusError(http.StatusUnauthorized, ""), + api.NewStatusError(http.StatusServiceUnavailable, ""), + api.NewStatusError(http.StatusInsufficientStorage, ""), + ) + require.Error(t, err, "expected error to be returned") - t.Run("MultiErrors", func(t *testing.T) {}) + _, ok := err.(*api.MultiStatusError) + require.True(t, ok, "expected error to be a multi-status error") + require.EqualError(t, err, "after 3 attempts: [507]: Insufficient Storage") + }) - t.Run("Mixed", func(t *testing.T) {}) + t.Run("MultiErrors", func(t *testing.T) { + err := api.JoinStatusErrors(2, 727*time.Millisecond, + errors.New("oopsie"), errors.New("something went wrong"), + ) + require.Error(t, err, "expected error to be returned") - t.Run("Deduplication", func(t *testing.T) {}) + _, ok := err.(*api.MultiStatusError) + require.True(t, ok, "expected error to be a multi-status error") + require.EqualError(t, err, "after 2 attempts: something went wrong") + }) + + t.Run("Mixed", func(t *testing.T) { + err := api.JoinStatusErrors(2, 3217*time.Millisecond, + api.NewStatusError(http.StatusServiceUnavailable, ""), + errors.New("something went wrong"), + ) + require.Error(t, err, "expected error to be returned") + + _, ok := err.(*api.MultiStatusError) + require.True(t, ok, "expected error to be a multi-status error") + require.EqualError(t, err, "after 2 attempts: something went wrong") + }) - t.Run("MultiDeduplication", func(t *testing.T) {}) + t.Run("Deduplication", func(t *testing.T) { + err := api.JoinStatusErrors(3, 2451*time.Millisecond, + api.NewStatusError(http.StatusServiceUnavailable, ""), + api.NewStatusError(http.StatusServiceUnavailable, ""), + api.NewStatusError(http.StatusServiceUnavailable, ""), + ) + require.Error(t, err, "expected error to be returned") + + _, ok := err.(*api.StatusError) + require.True(t, ok, "expected error to be a status error") + require.EqualError(t, err, "[503]: Service Unavailable") + }) + + t.Run("MultiDeduplication", func(t *testing.T) { + err := api.JoinStatusErrors(5, 3257*time.Millisecond, + api.NewStatusError(http.StatusUnauthorized, ""), + api.NewStatusError(http.StatusServiceUnavailable, ""), + api.NewStatusError(http.StatusUnauthorized, ""), + api.NewStatusError(http.StatusInsufficientStorage, ""), + api.NewStatusError(http.StatusServiceUnavailable, ""), + ) + require.Error(t, err, "expected error to be returned") + + _, ok := err.(*api.MultiStatusError) + require.True(t, ok, "expected error to be a multi-status error") + require.EqualError(t, err, "after 5 attempts: [507]: Insufficient Storage") + }) } func TestMultiStatusError(t *testing.T) {