github.com/sacloud/iaas-api-go@v1.12.0/client_test.go (about)

     1  // Copyright 2022-2023 The sacloud/iaas-api-go Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package iaas
    16  
    17  import (
    18  	"context"
    19  	"net/http"
    20  	"net/http/httptest"
    21  	"testing"
    22  	"time"
    23  
    24  	client "github.com/sacloud/api-client-go"
    25  	"github.com/stretchr/testify/require"
    26  )
    27  
    28  type dummyHandler struct {
    29  	called       []time.Time
    30  	responseCode int
    31  }
    32  
    33  func (s *dummyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    34  	if s.responseCode == http.StatusMovedPermanently {
    35  		w.Header().Set("Location", "/index.html")
    36  	}
    37  	w.WriteHeader(s.responseCode)
    38  	switch s.responseCode {
    39  	case http.StatusMultipleChoices,
    40  		http.StatusMovedPermanently,
    41  		http.StatusFound,
    42  		http.StatusSeeOther,
    43  		http.StatusNotModified,
    44  		http.StatusUseProxy,
    45  		http.StatusTemporaryRedirect,
    46  		http.StatusPermanentRedirect:
    47  		s.responseCode = http.StatusOK
    48  	default:
    49  		s.called = append(s.called, time.Now())
    50  	}
    51  }
    52  
    53  func (s *dummyHandler) isRetried() bool {
    54  	return len(s.called) > 1
    55  }
    56  
    57  func TestClient_Do_CheckRetryWithContext(t *testing.T) {
    58  	caller := NewClientWithOptions(&client.Options{
    59  		RetryMax:     1,
    60  		RetryWaitMin: 1,
    61  		RetryWaitMax: 1,
    62  	})
    63  
    64  	t.Run("context.Canceled", func(t *testing.T) {
    65  		h := &dummyHandler{
    66  			responseCode: http.StatusServiceUnavailable,
    67  		}
    68  		dummyServer := httptest.NewServer(h)
    69  		defer dummyServer.Close()
    70  
    71  		ctx, cancel := context.WithCancel(context.Background())
    72  		// make ctx to Canceled
    73  		cancel()
    74  
    75  		caller.Do(ctx, http.MethodGet, dummyServer.URL, nil) //nolint
    76  		require.False(t, h.isRetried(), "don't retry when context was canceled")
    77  	})
    78  
    79  	t.Run("context.DeadlineExceeded", func(t *testing.T) {
    80  		h := &dummyHandler{
    81  			responseCode: http.StatusServiceUnavailable,
    82  		}
    83  		dummyServer := httptest.NewServer(h)
    84  		defer dummyServer.Close()
    85  
    86  		ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
    87  		defer cancel()
    88  		// make ctx to DeadlineExceeded
    89  		time.Sleep(time.Millisecond)
    90  
    91  		caller.Do(ctx, http.MethodGet, dummyServer.URL, nil) //nolint
    92  		require.False(t, h.isRetried(), "don't retry when context exceeded deadline")
    93  	})
    94  }
    95  
    96  func TestClient_RetryByStatusCode(t *testing.T) {
    97  	cases := []struct {
    98  		responseCode int
    99  		shouldRetry  bool
   100  	}{
   101  		{responseCode: http.StatusOK, shouldRetry: false},
   102  		{responseCode: http.StatusCreated, shouldRetry: false},
   103  		{responseCode: http.StatusAccepted, shouldRetry: false},
   104  		{responseCode: http.StatusNoContent, shouldRetry: false},
   105  		{responseCode: http.StatusMovedPermanently, shouldRetry: false},
   106  		{responseCode: http.StatusFound, shouldRetry: false},
   107  		{responseCode: http.StatusBadRequest, shouldRetry: false},
   108  		{responseCode: http.StatusUnauthorized, shouldRetry: false},
   109  		{responseCode: http.StatusForbidden, shouldRetry: false},
   110  		{responseCode: http.StatusNotFound, shouldRetry: false},
   111  		{responseCode: http.StatusLocked, shouldRetry: true}, // Locked: 423
   112  		{responseCode: http.StatusInternalServerError, shouldRetry: false},
   113  		{responseCode: http.StatusBadGateway, shouldRetry: false},
   114  		{responseCode: http.StatusServiceUnavailable, shouldRetry: true},
   115  		{responseCode: http.StatusGatewayTimeout, shouldRetry: false},
   116  	}
   117  
   118  	caller := NewClientWithOptions(&client.Options{
   119  		RetryMax:     1,
   120  		RetryWaitMin: 1,
   121  		RetryWaitMax: 1,
   122  	})
   123  
   124  	for _, tt := range cases {
   125  		h := &dummyHandler{
   126  			responseCode: tt.responseCode,
   127  		}
   128  		dummyServer := httptest.NewServer(h)
   129  		caller.Do(context.Background(), http.MethodGet, dummyServer.URL, nil) //nolint
   130  		dummyServer.Close()
   131  
   132  		require.Equal(t, tt.shouldRetry, h.isRetried(),
   133  			"got unexpected retry status with status[%d]: expected:%t got:%t", tt.responseCode, tt.shouldRetry, h.isRetried())
   134  	}
   135  }