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 }