github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/testing/provider_client_test.go (about)

     1  package testing
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"sync/atomic"
    15  	"testing"
    16  	"time"
    17  
    18  	"github.com/vnpaycloud-console/gophercloud/v2"
    19  	th "github.com/vnpaycloud-console/gophercloud/v2/testhelper"
    20  	"github.com/vnpaycloud-console/gophercloud/v2/testhelper/client"
    21  )
    22  
    23  func TestAuthenticatedHeaders(t *testing.T) {
    24  	p := &gophercloud.ProviderClient{
    25  		TokenID: "1234",
    26  	}
    27  	expected := map[string]string{"X-Auth-Token": "1234"}
    28  	actual := p.AuthenticatedHeaders()
    29  	th.CheckDeepEquals(t, expected, actual)
    30  }
    31  
    32  func TestUserAgent(t *testing.T) {
    33  	p := &gophercloud.ProviderClient{}
    34  
    35  	p.UserAgent.Prepend("custom-user-agent/2.4.0")
    36  	expected := "custom-user-agent/2.4.0 " + gophercloud.DefaultUserAgent
    37  	actual := p.UserAgent.Join()
    38  	th.CheckEquals(t, expected, actual)
    39  
    40  	p.UserAgent.Prepend("another-custom-user-agent/0.3.0", "a-third-ua/5.9.0")
    41  	expected = "another-custom-user-agent/0.3.0 a-third-ua/5.9.0 custom-user-agent/2.4.0 " + gophercloud.DefaultUserAgent
    42  	actual = p.UserAgent.Join()
    43  	th.CheckEquals(t, expected, actual)
    44  
    45  	p.UserAgent = gophercloud.UserAgent{}
    46  	expected = gophercloud.DefaultUserAgent
    47  	actual = p.UserAgent.Join()
    48  	th.CheckEquals(t, expected, actual)
    49  }
    50  
    51  func TestConcurrentReauth(t *testing.T) {
    52  	var info = struct {
    53  		numreauths  int
    54  		failedAuths int
    55  		mut         *sync.RWMutex
    56  	}{
    57  		0,
    58  		0,
    59  		new(sync.RWMutex),
    60  	}
    61  
    62  	numconc := 20
    63  
    64  	prereauthTok := client.TokenID
    65  	postreauthTok := "12345678"
    66  
    67  	p := new(gophercloud.ProviderClient)
    68  	p.UseTokenLock()
    69  	p.SetToken(prereauthTok)
    70  	p.ReauthFunc = func(_ context.Context) error {
    71  		p.SetThrowaway(true)
    72  		time.Sleep(1 * time.Second)
    73  		p.AuthenticatedHeaders()
    74  		info.mut.Lock()
    75  		info.numreauths++
    76  		info.mut.Unlock()
    77  		p.TokenID = postreauthTok
    78  		p.SetThrowaway(false)
    79  		return nil
    80  	}
    81  
    82  	th.SetupHTTP()
    83  	defer th.TeardownHTTP()
    84  
    85  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
    86  		if r.Header.Get("X-Auth-Token") != postreauthTok {
    87  			w.WriteHeader(http.StatusUnauthorized)
    88  			info.mut.Lock()
    89  			info.failedAuths++
    90  			info.mut.Unlock()
    91  			return
    92  		}
    93  		info.mut.RLock()
    94  		hasReauthed := info.numreauths != 0
    95  		info.mut.RUnlock()
    96  
    97  		if hasReauthed {
    98  			th.CheckEquals(t, p.Token(), postreauthTok)
    99  		}
   100  
   101  		w.Header().Add("Content-Type", "application/json")
   102  		fmt.Fprint(w, `{}`)
   103  	})
   104  
   105  	wg := new(sync.WaitGroup)
   106  	reqopts := new(gophercloud.RequestOpts)
   107  	reqopts.KeepResponseBody = true
   108  	reqopts.MoreHeaders = map[string]string{
   109  		"X-Auth-Token": prereauthTok,
   110  	}
   111  
   112  	for i := 0; i < numconc; i++ {
   113  		wg.Add(1)
   114  		go func() {
   115  			defer wg.Done()
   116  			resp, err := p.Request(context.TODO(), "GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts)
   117  			th.CheckNoErr(t, err)
   118  			if resp == nil {
   119  				t.Errorf("got a nil response")
   120  				return
   121  			}
   122  			if resp.Body == nil {
   123  				t.Errorf("response body was nil")
   124  				return
   125  			}
   126  			defer resp.Body.Close()
   127  			actual, err := io.ReadAll(resp.Body)
   128  			if err != nil {
   129  				t.Errorf("error reading response body: %s", err)
   130  				return
   131  			}
   132  			th.CheckByteArrayEquals(t, []byte(`{}`), actual)
   133  		}()
   134  	}
   135  
   136  	wg.Wait()
   137  
   138  	th.AssertEquals(t, 1, info.numreauths)
   139  }
   140  
   141  func TestReauthEndLoop(t *testing.T) {
   142  	var info = struct {
   143  		reauthAttempts   int
   144  		maxReauthReached bool
   145  		mut              *sync.RWMutex
   146  	}{
   147  		0,
   148  		false,
   149  		new(sync.RWMutex),
   150  	}
   151  
   152  	numconc := 20
   153  	mut := new(sync.RWMutex)
   154  
   155  	p := new(gophercloud.ProviderClient)
   156  	p.UseTokenLock()
   157  	p.SetToken(client.TokenID)
   158  	p.ReauthFunc = func(_ context.Context) error {
   159  		info.mut.Lock()
   160  		defer info.mut.Unlock()
   161  
   162  		if info.reauthAttempts > 5 {
   163  			info.maxReauthReached = true
   164  			return fmt.Errorf("Max reauthentication attempts reached")
   165  		}
   166  		p.SetThrowaway(true)
   167  		p.AuthenticatedHeaders()
   168  		p.SetThrowaway(false)
   169  		info.reauthAttempts++
   170  
   171  		return nil
   172  	}
   173  
   174  	th.SetupHTTP()
   175  	defer th.TeardownHTTP()
   176  
   177  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   178  		// route always return 401
   179  		w.WriteHeader(http.StatusUnauthorized)
   180  	})
   181  
   182  	reqopts := new(gophercloud.RequestOpts)
   183  
   184  	// counters for the upcoming errors
   185  	errAfter := 0
   186  	errUnable := 0
   187  
   188  	wg := new(sync.WaitGroup)
   189  	for i := 0; i < numconc; i++ {
   190  		wg.Add(1)
   191  		go func() {
   192  			defer wg.Done()
   193  			_, err := p.Request(context.TODO(), "GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts)
   194  
   195  			mut.Lock()
   196  			defer mut.Unlock()
   197  
   198  			// ErrErrorAfter... will happen after a successful reauthentication,
   199  			// but the service still responds with a 401.
   200  			if _, ok := err.(*gophercloud.ErrErrorAfterReauthentication); ok {
   201  				errAfter++
   202  			}
   203  
   204  			// ErrErrorUnable... will happen when the custom reauth func reports
   205  			// an error.
   206  			if _, ok := err.(*gophercloud.ErrUnableToReauthenticate); ok {
   207  				errUnable++
   208  			}
   209  		}()
   210  	}
   211  
   212  	wg.Wait()
   213  	th.AssertEquals(t, info.reauthAttempts, 6)
   214  	th.AssertEquals(t, info.maxReauthReached, true)
   215  	th.AssertEquals(t, errAfter > 1, true)
   216  	th.AssertEquals(t, errUnable < 20, true)
   217  }
   218  
   219  func TestRequestThatCameDuringReauthWaitsUntilItIsCompleted(t *testing.T) {
   220  	var info = struct {
   221  		numreauths  int
   222  		failedAuths int
   223  		reauthCh    chan struct{}
   224  		mut         *sync.RWMutex
   225  	}{
   226  		0,
   227  		0,
   228  		make(chan struct{}),
   229  		new(sync.RWMutex),
   230  	}
   231  
   232  	numconc := 20
   233  
   234  	prereauthTok := client.TokenID
   235  	postreauthTok := "12345678"
   236  
   237  	p := new(gophercloud.ProviderClient)
   238  	p.UseTokenLock()
   239  	p.SetToken(prereauthTok)
   240  	p.ReauthFunc = func(_ context.Context) error {
   241  		info.mut.RLock()
   242  		if info.numreauths == 0 {
   243  			info.mut.RUnlock()
   244  			close(info.reauthCh)
   245  			time.Sleep(1 * time.Second)
   246  		} else {
   247  			info.mut.RUnlock()
   248  		}
   249  		p.SetThrowaway(true)
   250  		p.AuthenticatedHeaders()
   251  		info.mut.Lock()
   252  		info.numreauths++
   253  		info.mut.Unlock()
   254  		p.TokenID = postreauthTok
   255  		p.SetThrowaway(false)
   256  		return nil
   257  	}
   258  
   259  	th.SetupHTTP()
   260  	defer th.TeardownHTTP()
   261  
   262  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   263  		if r.Header.Get("X-Auth-Token") != postreauthTok {
   264  			info.mut.Lock()
   265  			info.failedAuths++
   266  			info.mut.Unlock()
   267  			w.WriteHeader(http.StatusUnauthorized)
   268  			return
   269  		}
   270  		info.mut.RLock()
   271  		hasReauthed := info.numreauths != 0
   272  		info.mut.RUnlock()
   273  
   274  		if hasReauthed {
   275  			th.CheckEquals(t, p.Token(), postreauthTok)
   276  		}
   277  
   278  		w.Header().Add("Content-Type", "application/json")
   279  		fmt.Fprint(w, `{}`)
   280  	})
   281  
   282  	wg := new(sync.WaitGroup)
   283  	reqopts := new(gophercloud.RequestOpts)
   284  	reqopts.KeepResponseBody = true
   285  	reqopts.MoreHeaders = map[string]string{
   286  		"X-Auth-Token": prereauthTok,
   287  	}
   288  
   289  	for i := 0; i < numconc; i++ {
   290  		wg.Add(1)
   291  		go func(i int) {
   292  			defer wg.Done()
   293  			if i != 0 {
   294  				<-info.reauthCh
   295  			}
   296  			resp, err := p.Request(context.TODO(), "GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts)
   297  			th.CheckNoErr(t, err)
   298  			if resp == nil {
   299  				t.Errorf("got a nil response")
   300  				return
   301  			}
   302  			if resp.Body == nil {
   303  				t.Errorf("response body was nil")
   304  				return
   305  			}
   306  			defer resp.Body.Close()
   307  			actual, err := io.ReadAll(resp.Body)
   308  			if err != nil {
   309  				t.Errorf("error reading response body: %s", err)
   310  				return
   311  			}
   312  			th.CheckByteArrayEquals(t, []byte(`{}`), actual)
   313  		}(i)
   314  	}
   315  
   316  	wg.Wait()
   317  
   318  	th.AssertEquals(t, 1, info.numreauths)
   319  	th.AssertEquals(t, 1, info.failedAuths)
   320  }
   321  
   322  func TestRequestReauthsAtMostOnce(t *testing.T) {
   323  	// There was an issue where Gophercloud would go into an infinite
   324  	// reauthentication loop with buggy services that send 401 even for fresh
   325  	// tokens. This test simulates such a service and checks that a call to
   326  	// ProviderClient.Request() will not try to reauthenticate more than once.
   327  
   328  	reauthCounter := 0
   329  	var reauthCounterMutex sync.Mutex
   330  
   331  	p := new(gophercloud.ProviderClient)
   332  	p.UseTokenLock()
   333  	p.SetToken(client.TokenID)
   334  	p.ReauthFunc = func(_ context.Context) error {
   335  		reauthCounterMutex.Lock()
   336  		reauthCounter++
   337  		reauthCounterMutex.Unlock()
   338  		//The actual token value does not matter, the endpoint does not check it.
   339  		return nil
   340  	}
   341  
   342  	th.SetupHTTP()
   343  	defer th.TeardownHTTP()
   344  
   345  	requestCounter := 0
   346  	var requestCounterMutex sync.Mutex
   347  
   348  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   349  		requestCounterMutex.Lock()
   350  		requestCounter++
   351  		//avoid infinite loop
   352  		if requestCounter == 10 {
   353  			http.Error(w, "too many requests", http.StatusTooManyRequests)
   354  			return
   355  		}
   356  		requestCounterMutex.Unlock()
   357  
   358  		//always reply 401, even immediately after reauthenticate
   359  		http.Error(w, "unauthorized", http.StatusUnauthorized)
   360  	})
   361  
   362  	// The expected error message indicates that we reauthenticated once (that's
   363  	// the part before the colon), but when encountering another 401 response, we
   364  	// did not attempt reauthentication again and just passed that 401 response to
   365  	// the caller as ErrDefault401.
   366  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   367  	expectedErrorRx := regexp.MustCompile(`^Successfully re-authenticated, but got error executing request: Expected HTTP response code \[200\] when accessing \[GET http://[^/]*//route\], but got 401 instead: unauthorized$`)
   368  	if !expectedErrorRx.MatchString(err.Error()) {
   369  		t.Errorf("expected error that looks like %q, but got %q", expectedErrorRx.String(), err.Error())
   370  	}
   371  }
   372  
   373  func TestRequestWithContext(t *testing.T) {
   374  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   375  		fmt.Fprintln(w, "OK")
   376  	}))
   377  	defer ts.Close()
   378  
   379  	ctx, cancel := context.WithCancel(context.Background())
   380  	p := &gophercloud.ProviderClient{}
   381  
   382  	res, err := p.Request(ctx, "GET", ts.URL, &gophercloud.RequestOpts{KeepResponseBody: true})
   383  	th.AssertNoErr(t, err)
   384  	_, err = io.ReadAll(res.Body)
   385  	th.AssertNoErr(t, err)
   386  	err = res.Body.Close()
   387  	th.AssertNoErr(t, err)
   388  
   389  	cancel()
   390  	_, err = p.Request(ctx, "GET", ts.URL, &gophercloud.RequestOpts{})
   391  	if err == nil {
   392  		t.Fatal("expecting error, got nil")
   393  	}
   394  	if !strings.Contains(err.Error(), ctx.Err().Error()) {
   395  		t.Fatalf("expecting error to contain: %q, got %q", ctx.Err().Error(), err.Error())
   396  	}
   397  }
   398  
   399  func TestRequestConnectionReuse(t *testing.T) {
   400  	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   401  		fmt.Fprintln(w, "OK")
   402  	}))
   403  
   404  	// an amount of iterations
   405  	var iter = 10000
   406  	// connections tracks an amount of connections made
   407  	var connections int64
   408  
   409  	ts.Config.ConnState = func(_ net.Conn, s http.ConnState) {
   410  		// track an amount of connections
   411  		if s == http.StateNew {
   412  			atomic.AddInt64(&connections, 1)
   413  		}
   414  	}
   415  	ts.Start()
   416  	defer ts.Close()
   417  
   418  	p := &gophercloud.ProviderClient{}
   419  	for i := 0; i < iter; i++ {
   420  		_, err := p.Request(context.TODO(), "GET", ts.URL, &gophercloud.RequestOpts{KeepResponseBody: false})
   421  		th.AssertNoErr(t, err)
   422  	}
   423  
   424  	th.AssertEquals(t, int64(1), connections)
   425  }
   426  
   427  func TestRequestConnectionClose(t *testing.T) {
   428  	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   429  		fmt.Fprintln(w, "OK")
   430  	}))
   431  
   432  	// an amount of iterations
   433  	var iter = 10
   434  	// connections tracks an amount of connections made
   435  	var connections int64
   436  
   437  	ts.Config.ConnState = func(_ net.Conn, s http.ConnState) {
   438  		// track an amount of connections
   439  		if s == http.StateNew {
   440  			atomic.AddInt64(&connections, 1)
   441  		}
   442  	}
   443  	ts.Start()
   444  	defer ts.Close()
   445  
   446  	p := &gophercloud.ProviderClient{}
   447  	for i := 0; i < iter; i++ {
   448  		_, err := p.Request(context.TODO(), "GET", ts.URL, &gophercloud.RequestOpts{KeepResponseBody: true})
   449  		th.AssertNoErr(t, err)
   450  	}
   451  
   452  	th.AssertEquals(t, int64(iter), connections)
   453  }
   454  
   455  func retryBackoffTest(retryCounter *uint, t *testing.T) gophercloud.RetryBackoffFunc {
   456  	return func(ctx context.Context, respErr *gophercloud.ErrUnexpectedResponseCode, e error, retries uint) error {
   457  		retryAfter := respErr.ResponseHeader.Get("Retry-After")
   458  		if retryAfter == "" {
   459  			return e
   460  		}
   461  
   462  		var sleep time.Duration
   463  
   464  		// Parse delay seconds or HTTP date
   465  		if v, err := strconv.ParseUint(retryAfter, 10, 32); err == nil {
   466  			sleep = time.Duration(v) * time.Second
   467  		} else if v, err := time.Parse(http.TimeFormat, retryAfter); err == nil {
   468  			sleep = time.Until(v)
   469  		} else {
   470  			return e
   471  		}
   472  
   473  		if ctx != nil {
   474  			t.Logf("Context sleeping for %d milliseconds", sleep.Milliseconds())
   475  			select {
   476  			case <-time.After(sleep):
   477  				t.Log("sleep is over")
   478  			case <-ctx.Done():
   479  				t.Log(ctx.Err())
   480  				return e
   481  			}
   482  		} else {
   483  			t.Logf("Sleeping for %d milliseconds", sleep.Milliseconds())
   484  			time.Sleep(sleep)
   485  			t.Log("sleep is over")
   486  		}
   487  
   488  		*retryCounter = *retryCounter + 1
   489  
   490  		return nil
   491  	}
   492  }
   493  
   494  func TestRequestRetry(t *testing.T) {
   495  	var retryCounter uint
   496  
   497  	p := &gophercloud.ProviderClient{}
   498  	p.UseTokenLock()
   499  	p.SetToken(client.TokenID)
   500  	p.MaxBackoffRetries = 3
   501  
   502  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   503  
   504  	th.SetupHTTP()
   505  	defer th.TeardownHTTP()
   506  
   507  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   508  		w.Header().Set("Retry-After", "1")
   509  
   510  		//always reply 429
   511  		http.Error(w, "retry later", http.StatusTooManyRequests)
   512  	})
   513  
   514  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   515  	if err == nil {
   516  		t.Fatal("expecting error, got nil")
   517  	}
   518  	th.AssertEquals(t, retryCounter, p.MaxBackoffRetries)
   519  }
   520  
   521  func TestRequestRetryHTTPDate(t *testing.T) {
   522  	var retryCounter uint
   523  
   524  	p := &gophercloud.ProviderClient{}
   525  	p.UseTokenLock()
   526  	p.SetToken(client.TokenID)
   527  	p.MaxBackoffRetries = 3
   528  
   529  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   530  
   531  	th.SetupHTTP()
   532  	defer th.TeardownHTTP()
   533  
   534  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   535  		w.Header().Set("Retry-After", time.Now().Add(1*time.Second).UTC().Format(http.TimeFormat))
   536  
   537  		//always reply 429
   538  		http.Error(w, "retry later", http.StatusTooManyRequests)
   539  	})
   540  
   541  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   542  	if err == nil {
   543  		t.Fatal("expecting error, got nil")
   544  	}
   545  	th.AssertEquals(t, retryCounter, p.MaxBackoffRetries)
   546  }
   547  
   548  func TestRequestRetryError(t *testing.T) {
   549  	var retryCounter uint
   550  
   551  	p := &gophercloud.ProviderClient{}
   552  	p.UseTokenLock()
   553  	p.SetToken(client.TokenID)
   554  	p.MaxBackoffRetries = 3
   555  
   556  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   557  
   558  	th.SetupHTTP()
   559  	defer th.TeardownHTTP()
   560  
   561  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   562  		w.Header().Set("Retry-After", "foo bar")
   563  
   564  		//always reply 429
   565  		http.Error(w, "retry later", http.StatusTooManyRequests)
   566  	})
   567  
   568  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   569  	if err == nil {
   570  		t.Fatal("expecting error, got nil")
   571  	}
   572  	th.AssertEquals(t, retryCounter, uint(0))
   573  }
   574  
   575  func TestRequestRetrySuccess(t *testing.T) {
   576  	var retryCounter uint
   577  
   578  	p := &gophercloud.ProviderClient{}
   579  	p.UseTokenLock()
   580  	p.SetToken(client.TokenID)
   581  	p.MaxBackoffRetries = 3
   582  
   583  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   584  
   585  	th.SetupHTTP()
   586  	defer th.TeardownHTTP()
   587  
   588  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   589  		//always reply 200
   590  		http.Error(w, "retry later", http.StatusOK)
   591  	})
   592  
   593  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   594  	if err != nil {
   595  		t.Fatal(err)
   596  	}
   597  	th.AssertEquals(t, retryCounter, uint(0))
   598  }
   599  
   600  func TestRequestRetryContext(t *testing.T) {
   601  	var retryCounter uint
   602  
   603  	ctx, cancel := context.WithCancel(context.Background())
   604  	go func() {
   605  		sleep := 2.5 * 1000 * time.Millisecond
   606  		time.Sleep(sleep)
   607  		cancel()
   608  	}()
   609  
   610  	p := &gophercloud.ProviderClient{}
   611  	p.UseTokenLock()
   612  	p.SetToken(client.TokenID)
   613  	p.MaxBackoffRetries = 3
   614  
   615  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   616  
   617  	th.SetupHTTP()
   618  	defer th.TeardownHTTP()
   619  
   620  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   621  		w.Header().Set("Retry-After", "1")
   622  
   623  		//always reply 429
   624  		http.Error(w, "retry later", http.StatusTooManyRequests)
   625  	})
   626  
   627  	_, err := p.Request(ctx, "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   628  	if err == nil {
   629  		t.Fatal("expecting error, got nil")
   630  	}
   631  	t.Logf("retryCounter: %d, p.MaxBackoffRetries: %d", retryCounter, p.MaxBackoffRetries-1)
   632  	th.AssertEquals(t, retryCounter, p.MaxBackoffRetries-1)
   633  }
   634  
   635  func TestRequestGeneralRetry(t *testing.T) {
   636  	p := &gophercloud.ProviderClient{}
   637  	p.UseTokenLock()
   638  	p.SetToken(client.TokenID)
   639  	p.RetryFunc = func(context context.Context, method, url string, options *gophercloud.RequestOpts, err error, failCount uint) error {
   640  		if failCount >= 5 {
   641  			return err
   642  		}
   643  		return nil
   644  	}
   645  
   646  	th.SetupHTTP()
   647  	defer th.TeardownHTTP()
   648  
   649  	count := 0
   650  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   651  		if count < 3 {
   652  			http.Error(w, "bad gateway", http.StatusBadGateway)
   653  			count += 1
   654  		} else {
   655  			fmt.Fprintln(w, "OK")
   656  		}
   657  	})
   658  
   659  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   660  	if err != nil {
   661  		t.Fatal("expecting nil, got err")
   662  	}
   663  	th.AssertEquals(t, 3, count)
   664  }
   665  
   666  func TestRequestGeneralRetryAbort(t *testing.T) {
   667  	p := &gophercloud.ProviderClient{}
   668  	p.UseTokenLock()
   669  	p.SetToken(client.TokenID)
   670  	p.RetryFunc = func(context context.Context, method, url string, options *gophercloud.RequestOpts, err error, failCount uint) error {
   671  		return err
   672  	}
   673  
   674  	th.SetupHTTP()
   675  	defer th.TeardownHTTP()
   676  
   677  	count := 0
   678  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   679  		if count < 3 {
   680  			http.Error(w, "bad gateway", http.StatusBadGateway)
   681  			count += 1
   682  		} else {
   683  			fmt.Fprintln(w, "OK")
   684  		}
   685  	})
   686  
   687  	_, err := p.Request(context.TODO(), "GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   688  	if err == nil {
   689  		t.Fatal("expecting err, got nil")
   690  	}
   691  	th.AssertEquals(t, 1, count)
   692  }
   693  
   694  func TestRequestWrongOkCode(t *testing.T) {
   695  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   696  		fmt.Fprintln(w, "OK")
   697  		// Returns 200 OK
   698  	}))
   699  	defer ts.Close()
   700  
   701  	p := &gophercloud.ProviderClient{}
   702  
   703  	_, err := p.Request(context.TODO(), "DELETE", ts.URL, &gophercloud.RequestOpts{})
   704  	th.AssertErr(t, err)
   705  	if urErr, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok {
   706  		// DELETE expects a 202 or 204 by default
   707  		// Make sure returned error contains the expected OK codes
   708  		th.AssertDeepEquals(t, []int{202, 204}, urErr.Expected)
   709  	} else {
   710  		t.Fatalf("expected error type gophercloud.ErrUnexpectedResponseCode but got %T", err)
   711  	}
   712  }