github.com/gophercloud/gophercloud@v1.11.0/testing/provider_client_test.go (about)

     1  package testing
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net"
     8  	"net/http"
     9  	"net/http/httptest"
    10  	"strconv"
    11  	"strings"
    12  	"sync"
    13  	"sync/atomic"
    14  	"testing"
    15  	"time"
    16  
    17  	"github.com/gophercloud/gophercloud"
    18  	th "github.com/gophercloud/gophercloud/testhelper"
    19  	"github.com/gophercloud/gophercloud/testhelper/client"
    20  )
    21  
    22  func TestAuthenticatedHeaders(t *testing.T) {
    23  	p := &gophercloud.ProviderClient{
    24  		TokenID: "1234",
    25  	}
    26  	expected := map[string]string{"X-Auth-Token": "1234"}
    27  	actual := p.AuthenticatedHeaders()
    28  	th.CheckDeepEquals(t, expected, actual)
    29  }
    30  
    31  func TestUserAgent(t *testing.T) {
    32  	p := &gophercloud.ProviderClient{}
    33  
    34  	p.UserAgent.Prepend("custom-user-agent/2.4.0")
    35  	expected := "custom-user-agent/2.4.0 " + gophercloud.DefaultUserAgent
    36  	actual := p.UserAgent.Join()
    37  	th.CheckEquals(t, expected, actual)
    38  
    39  	p.UserAgent.Prepend("another-custom-user-agent/0.3.0", "a-third-ua/5.9.0")
    40  	expected = "another-custom-user-agent/0.3.0 a-third-ua/5.9.0 custom-user-agent/2.4.0 " + gophercloud.DefaultUserAgent
    41  	actual = p.UserAgent.Join()
    42  	th.CheckEquals(t, expected, actual)
    43  
    44  	p.UserAgent = gophercloud.UserAgent{}
    45  	expected = gophercloud.DefaultUserAgent
    46  	actual = p.UserAgent.Join()
    47  	th.CheckEquals(t, expected, actual)
    48  }
    49  
    50  func TestConcurrentReauth(t *testing.T) {
    51  	var info = struct {
    52  		numreauths  int
    53  		failedAuths int
    54  		mut         *sync.RWMutex
    55  	}{
    56  		0,
    57  		0,
    58  		new(sync.RWMutex),
    59  	}
    60  
    61  	numconc := 20
    62  
    63  	prereauthTok := client.TokenID
    64  	postreauthTok := "12345678"
    65  
    66  	p := new(gophercloud.ProviderClient)
    67  	p.UseTokenLock()
    68  	p.SetToken(prereauthTok)
    69  	p.ReauthFunc = func() error {
    70  		p.SetThrowaway(true)
    71  		time.Sleep(1 * time.Second)
    72  		p.AuthenticatedHeaders()
    73  		info.mut.Lock()
    74  		info.numreauths++
    75  		info.mut.Unlock()
    76  		p.TokenID = postreauthTok
    77  		p.SetThrowaway(false)
    78  		return nil
    79  	}
    80  
    81  	th.SetupHTTP()
    82  	defer th.TeardownHTTP()
    83  
    84  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
    85  		if r.Header.Get("X-Auth-Token") != postreauthTok {
    86  			w.WriteHeader(http.StatusUnauthorized)
    87  			info.mut.Lock()
    88  			info.failedAuths++
    89  			info.mut.Unlock()
    90  			return
    91  		}
    92  		info.mut.RLock()
    93  		hasReauthed := info.numreauths != 0
    94  		info.mut.RUnlock()
    95  
    96  		if hasReauthed {
    97  			th.CheckEquals(t, p.Token(), postreauthTok)
    98  		}
    99  
   100  		w.Header().Add("Content-Type", "application/json")
   101  		fmt.Fprintf(w, `{}`)
   102  	})
   103  
   104  	wg := new(sync.WaitGroup)
   105  	reqopts := new(gophercloud.RequestOpts)
   106  	reqopts.KeepResponseBody = true
   107  	reqopts.MoreHeaders = map[string]string{
   108  		"X-Auth-Token": prereauthTok,
   109  	}
   110  
   111  	for i := 0; i < numconc; i++ {
   112  		wg.Add(1)
   113  		go func() {
   114  			defer wg.Done()
   115  			resp, err := p.Request("GET", fmt.Sprintf("%s/route", th.Endpoint()), reqopts)
   116  			th.CheckNoErr(t, err)
   117  			if resp == nil {
   118  				t.Errorf("got a nil response")
   119  				return
   120  			}
   121  			if resp.Body == nil {
   122  				t.Errorf("response body was nil")
   123  				return
   124  			}
   125  			defer resp.Body.Close()
   126  			actual, err := ioutil.ReadAll(resp.Body)
   127  			if err != nil {
   128  				t.Errorf("error reading response body: %s", err)
   129  				return
   130  			}
   131  			th.CheckByteArrayEquals(t, []byte(`{}`), actual)
   132  		}()
   133  	}
   134  
   135  	wg.Wait()
   136  
   137  	th.AssertEquals(t, 1, info.numreauths)
   138  }
   139  
   140  func TestReauthEndLoop(t *testing.T) {
   141  	var info = struct {
   142  		reauthAttempts   int
   143  		maxReauthReached bool
   144  		mut              *sync.RWMutex
   145  	}{
   146  		0,
   147  		false,
   148  		new(sync.RWMutex),
   149  	}
   150  
   151  	numconc := 20
   152  	mut := new(sync.RWMutex)
   153  
   154  	p := new(gophercloud.ProviderClient)
   155  	p.UseTokenLock()
   156  	p.SetToken(client.TokenID)
   157  	p.ReauthFunc = func() error {
   158  		info.mut.Lock()
   159  		defer info.mut.Unlock()
   160  
   161  		if info.reauthAttempts > 5 {
   162  			info.maxReauthReached = true
   163  			return fmt.Errorf("Max reauthentication attempts reached")
   164  		}
   165  		p.SetThrowaway(true)
   166  		p.AuthenticatedHeaders()
   167  		p.SetThrowaway(false)
   168  		info.reauthAttempts++
   169  
   170  		return nil
   171  	}
   172  
   173  	th.SetupHTTP()
   174  	defer th.TeardownHTTP()
   175  
   176  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   177  		// route always return 401
   178  		w.WriteHeader(http.StatusUnauthorized)
   179  		return
   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("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() 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.Fprintf(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("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 := ioutil.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() 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("GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   367  	expectedErrorMessage := "Successfully re-authenticated, but got error executing request: Authentication failed"
   368  	th.AssertEquals(t, expectedErrorMessage, err.Error())
   369  }
   370  
   371  func TestRequestWithContext(t *testing.T) {
   372  	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   373  		fmt.Fprintln(w, "OK")
   374  	}))
   375  	defer ts.Close()
   376  
   377  	ctx, cancel := context.WithCancel(context.Background())
   378  	p := &gophercloud.ProviderClient{Context: ctx}
   379  
   380  	res, err := p.Request("GET", ts.URL, &gophercloud.RequestOpts{KeepResponseBody: true})
   381  	th.AssertNoErr(t, err)
   382  	_, err = ioutil.ReadAll(res.Body)
   383  	th.AssertNoErr(t, err)
   384  	err = res.Body.Close()
   385  	th.AssertNoErr(t, err)
   386  
   387  	cancel()
   388  	res, err = p.Request("GET", ts.URL, &gophercloud.RequestOpts{})
   389  	if err == nil {
   390  		t.Fatal("expecting error, got nil")
   391  	}
   392  	if !strings.Contains(err.Error(), ctx.Err().Error()) {
   393  		t.Fatalf("expecting error to contain: %q, got %q", ctx.Err().Error(), err.Error())
   394  	}
   395  }
   396  
   397  func TestRequestConnectionReuse(t *testing.T) {
   398  	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   399  		fmt.Fprintln(w, "OK")
   400  	}))
   401  
   402  	// an amount of iterations
   403  	var iter = 10000
   404  	// connections tracks an amount of connections made
   405  	var connections int64
   406  
   407  	ts.Config.ConnState = func(_ net.Conn, s http.ConnState) {
   408  		// track an amount of connections
   409  		if s == http.StateNew {
   410  			atomic.AddInt64(&connections, 1)
   411  		}
   412  	}
   413  	ts.Start()
   414  	defer ts.Close()
   415  
   416  	p := &gophercloud.ProviderClient{}
   417  	for i := 0; i < iter; i++ {
   418  		_, err := p.Request("GET", ts.URL, &gophercloud.RequestOpts{KeepResponseBody: false})
   419  		th.AssertNoErr(t, err)
   420  	}
   421  
   422  	th.AssertEquals(t, int64(1), connections)
   423  }
   424  
   425  func TestRequestConnectionClose(t *testing.T) {
   426  	ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   427  		fmt.Fprintln(w, "OK")
   428  	}))
   429  
   430  	// an amount of iterations
   431  	var iter = 10
   432  	// connections tracks an amount of connections made
   433  	var connections int64
   434  
   435  	ts.Config.ConnState = func(_ net.Conn, s http.ConnState) {
   436  		// track an amount of connections
   437  		if s == http.StateNew {
   438  			atomic.AddInt64(&connections, 1)
   439  		}
   440  	}
   441  	ts.Start()
   442  	defer ts.Close()
   443  
   444  	p := &gophercloud.ProviderClient{}
   445  	for i := 0; i < iter; i++ {
   446  		_, err := p.Request("GET", ts.URL, &gophercloud.RequestOpts{KeepResponseBody: true})
   447  		th.AssertNoErr(t, err)
   448  	}
   449  
   450  	th.AssertEquals(t, int64(iter), connections)
   451  }
   452  
   453  func retryBackoffTest(retryCounter *uint, t *testing.T) gophercloud.RetryBackoffFunc {
   454  	return func(ctx context.Context, respErr *gophercloud.ErrUnexpectedResponseCode, e error, retries uint) error {
   455  		retryAfter := respErr.ResponseHeader.Get("Retry-After")
   456  		if retryAfter == "" {
   457  			return e
   458  		}
   459  
   460  		var sleep time.Duration
   461  
   462  		// Parse delay seconds or HTTP date
   463  		if v, err := strconv.ParseUint(retryAfter, 10, 32); err == nil {
   464  			sleep = time.Duration(v) * time.Second
   465  		} else if v, err := time.Parse(http.TimeFormat, retryAfter); err == nil {
   466  			sleep = time.Until(v)
   467  		} else {
   468  			return e
   469  		}
   470  
   471  		if ctx != nil {
   472  			t.Logf("Context sleeping for %d milliseconds", sleep.Milliseconds())
   473  			select {
   474  			case <-time.After(sleep):
   475  				t.Log("sleep is over")
   476  			case <-ctx.Done():
   477  				t.Log(ctx.Err())
   478  				return e
   479  			}
   480  		} else {
   481  			t.Logf("Sleeping for %d milliseconds", sleep.Milliseconds())
   482  			time.Sleep(sleep)
   483  			t.Log("sleep is over")
   484  		}
   485  
   486  		*retryCounter = *retryCounter + 1
   487  
   488  		return nil
   489  	}
   490  }
   491  
   492  func TestRequestRetry(t *testing.T) {
   493  	var retryCounter uint
   494  
   495  	p := &gophercloud.ProviderClient{}
   496  	p.UseTokenLock()
   497  	p.SetToken(client.TokenID)
   498  	p.MaxBackoffRetries = 3
   499  
   500  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   501  
   502  	th.SetupHTTP()
   503  	defer th.TeardownHTTP()
   504  
   505  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   506  		w.Header().Set("Retry-After", "1")
   507  
   508  		//always reply 429
   509  		http.Error(w, "retry later", http.StatusTooManyRequests)
   510  	})
   511  
   512  	_, err := p.Request("GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   513  	if err == nil {
   514  		t.Fatal("expecting error, got nil")
   515  	}
   516  	th.AssertEquals(t, retryCounter, p.MaxBackoffRetries)
   517  }
   518  
   519  func TestRequestRetryHTTPDate(t *testing.T) {
   520  	var retryCounter uint
   521  
   522  	p := &gophercloud.ProviderClient{}
   523  	p.UseTokenLock()
   524  	p.SetToken(client.TokenID)
   525  	p.MaxBackoffRetries = 3
   526  
   527  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   528  
   529  	th.SetupHTTP()
   530  	defer th.TeardownHTTP()
   531  
   532  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   533  		w.Header().Set("Retry-After", time.Now().Add(1*time.Second).UTC().Format(http.TimeFormat))
   534  
   535  		//always reply 429
   536  		http.Error(w, "retry later", http.StatusTooManyRequests)
   537  	})
   538  
   539  	_, err := p.Request("GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   540  	if err == nil {
   541  		t.Fatal("expecting error, got nil")
   542  	}
   543  	th.AssertEquals(t, retryCounter, p.MaxBackoffRetries)
   544  }
   545  
   546  func TestRequestRetryError(t *testing.T) {
   547  	var retryCounter uint
   548  
   549  	p := &gophercloud.ProviderClient{}
   550  	p.UseTokenLock()
   551  	p.SetToken(client.TokenID)
   552  	p.MaxBackoffRetries = 3
   553  
   554  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   555  
   556  	th.SetupHTTP()
   557  	defer th.TeardownHTTP()
   558  
   559  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   560  		w.Header().Set("Retry-After", "foo bar")
   561  
   562  		//always reply 429
   563  		http.Error(w, "retry later", http.StatusTooManyRequests)
   564  	})
   565  
   566  	_, err := p.Request("GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   567  	if err == nil {
   568  		t.Fatal("expecting error, got nil")
   569  	}
   570  	th.AssertEquals(t, retryCounter, uint(0))
   571  }
   572  
   573  func TestRequestRetrySuccess(t *testing.T) {
   574  	var retryCounter uint
   575  
   576  	p := &gophercloud.ProviderClient{}
   577  	p.UseTokenLock()
   578  	p.SetToken(client.TokenID)
   579  	p.MaxBackoffRetries = 3
   580  
   581  	p.RetryBackoffFunc = retryBackoffTest(&retryCounter, t)
   582  
   583  	th.SetupHTTP()
   584  	defer th.TeardownHTTP()
   585  
   586  	th.Mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) {
   587  		//always reply 200
   588  		http.Error(w, "retry later", http.StatusOK)
   589  	})
   590  
   591  	_, err := p.Request("GET", th.Endpoint()+"/route", &gophercloud.RequestOpts{})
   592  	if err != nil {
   593  		t.Fatal(err)
   594  	}
   595  	th.AssertEquals(t, retryCounter, uint(0))
   596  }
   597  
   598  func TestRequestRetryContext(t *testing.T) {
   599  	var retryCounter uint
   600  
   601  	ctx, cancel := context.WithCancel(context.Background())
   602  	go func() {
   603  		sleep := 2.5 * 1000 * time.Millisecond
   604  		time.Sleep(sleep)
   605  		cancel()
   606  	}()
   607  
   608  	p := &gophercloud.ProviderClient{
   609  		Context: ctx,
   610  	}
   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("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("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("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("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  }