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 }