1package httpmock 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "io/ioutil" 9 "net" 10 "net/http" 11 "net/url" 12 "reflect" 13 "regexp" 14 "strings" 15 "testing" 16 "time" 17) 18 19var testURL = "http://www.example.com/" 20 21func assertBody(t *testing.T, resp *http.Response, expected string) { 22 defer resp.Body.Close() 23 24 data, err := ioutil.ReadAll(resp.Body) 25 if err != nil { 26 t.Fatal(err) 27 } 28 29 got := string(data) 30 31 if got != expected { 32 t.Errorf("Expected body: %#v, got %#v", expected, got) 33 } 34} 35 36func TestRouteKey(t *testing.T) { 37 got, expected := noResponder.String(), "NO_RESPONDER" 38 if got != expected { 39 t.Errorf("got: %v, expected: %v", got, expected) 40 } 41 42 got, expected = routeKey{Method: "GET", URL: "/foo"}.String(), "GET /foo" 43 if got != expected { 44 t.Errorf("got: %v, expected: %v", got, expected) 45 } 46} 47 48func TestMockTransport(t *testing.T) { 49 Activate() 50 defer Deactivate() 51 52 url := "https://github.com/" 53 body := `["hello world"]` + "\n" 54 55 RegisterResponder("GET", url, NewStringResponder(200, body)) 56 57 // Read it as a simple string (ioutil.ReadAll will trigger io.EOF) 58 func() { 59 resp, err := http.Get(url) 60 if err != nil { 61 t.Fatal(err) 62 } 63 defer resp.Body.Close() 64 65 data, err := ioutil.ReadAll(resp.Body) 66 if err != nil { 67 t.Fatal(err) 68 } 69 70 if string(data) != body { 71 t.FailNow() 72 } 73 74 // the http client wraps our NoResponderFound error, so we just try and match on text 75 if _, err := http.Get(testURL); !strings.Contains(err.Error(), 76 NoResponderFound.Error()) { 77 78 t.Fatal(err) 79 } 80 }() 81 82 // Do it again, but twice with json decoder (json Decode will not 83 // reach EOF, but Close is called as the JSON response is complete) 84 for i := 0; i < 2; i++ { 85 func() { 86 resp, err := http.Get(url) 87 if err != nil { 88 t.Fatal(err) 89 } 90 defer resp.Body.Close() 91 92 var res []string 93 err = json.NewDecoder(resp.Body).Decode(&res) 94 if err != nil { 95 t.Fatal(err) 96 } 97 98 if len(res) != 1 || res[0] != "hello world" { 99 t.Fatalf(`%v read instead of ["hello world"]`, res) 100 } 101 }() 102 } 103} 104 105// We should be able to find GET handlers when using an http.Request with a 106// default (zero-value) .Method. 107func TestMockTransportDefaultMethod(t *testing.T) { 108 Activate() 109 defer Deactivate() 110 111 const urlString = "https://github.com/" 112 url, err := url.Parse(urlString) 113 if err != nil { 114 t.Fatal(err) 115 } 116 body := "hello world" 117 118 RegisterResponder("GET", urlString, NewStringResponder(200, body)) 119 120 req := &http.Request{ 121 URL: url, 122 // Note: Method unspecified (zero-value) 123 } 124 125 client := &http.Client{} 126 resp, err := client.Do(req) 127 if err != nil { 128 t.Fatal(err) 129 } 130 defer resp.Body.Close() 131 132 data, err := ioutil.ReadAll(resp.Body) 133 if err != nil { 134 t.Fatal(err) 135 } 136 137 if string(data) != body { 138 t.FailNow() 139 } 140} 141 142func TestMockTransportReset(t *testing.T) { 143 DeactivateAndReset() 144 145 if len(DefaultTransport.responders) > 0 { 146 t.Fatal("expected no responders at this point") 147 } 148 149 RegisterResponder("GET", testURL, nil) 150 151 if len(DefaultTransport.responders) != 1 { 152 t.Fatal("expected one responder") 153 } 154 155 Reset() 156 157 if len(DefaultTransport.responders) > 0 { 158 t.Fatal("expected no responders as they were just reset") 159 } 160} 161 162func TestMockTransportNoResponder(t *testing.T) { 163 Activate() 164 defer DeactivateAndReset() 165 166 Reset() 167 168 if DefaultTransport.noResponder != nil { 169 t.Fatal("expected noResponder to be nil") 170 } 171 172 if _, err := http.Get(testURL); err == nil { 173 t.Fatal("expected to receive a connection error due to lack of responders") 174 } 175 176 RegisterNoResponder(NewStringResponder(200, "hello world")) 177 178 resp, err := http.Get(testURL) 179 if err != nil { 180 t.Fatal("expected request to succeed") 181 } 182 183 data, err := ioutil.ReadAll(resp.Body) 184 if err != nil { 185 t.Fatal(err) 186 } 187 188 if string(data) != "hello world" { 189 t.Fatal("expected body to be 'hello world'") 190 } 191} 192 193func TestMockTransportQuerystringFallback(t *testing.T) { 194 Activate() 195 defer DeactivateAndReset() 196 197 // register the testURL responder 198 RegisterResponder("GET", testURL, NewStringResponder(200, "hello world")) 199 200 for _, suffix := range []string{"?", "?hello=world", "?hello=world#foo", "?hello=world&hello=all", "#foo"} { 201 reqURL := testURL + suffix 202 203 // make a request for the testURL with a querystring 204 resp, err := http.Get(reqURL) 205 if err != nil { 206 t.Fatalf("expected request %s to succeed", reqURL) 207 } 208 209 data, err := ioutil.ReadAll(resp.Body) 210 if err != nil { 211 t.Fatalf("%s error: %s", reqURL, err) 212 } 213 214 if string(data) != "hello world" { 215 t.Fatalf("expected body of %s to be 'hello world'", reqURL) 216 } 217 } 218} 219 220func TestMockTransportPathOnlyFallback(t *testing.T) { 221 // Just in case a panic occurs 222 defer DeactivateAndReset() 223 224 for _, test := range []struct { 225 Responder string 226 Paths []string 227 }{ 228 { 229 // unsorted query string matches exactly 230 Responder: "/hello/world?query=string&abc=zz#fragment", 231 Paths: []string{ 232 testURL + "hello/world?query=string&abc=zz#fragment", 233 }, 234 }, 235 { 236 // sorted query string matches all cases 237 Responder: "/hello/world?abc=zz&query=string#fragment", 238 Paths: []string{ 239 testURL + "hello/world?query=string&abc=zz#fragment", 240 testURL + "hello/world?abc=zz&query=string#fragment", 241 }, 242 }, 243 { 244 // unsorted query string matches exactly 245 Responder: "/hello/world?query=string&abc=zz", 246 Paths: []string{ 247 testURL + "hello/world?query=string&abc=zz", 248 }, 249 }, 250 { 251 // sorted query string matches all cases 252 Responder: "/hello/world?abc=zz&query=string", 253 Paths: []string{ 254 testURL + "hello/world?query=string&abc=zz", 255 testURL + "hello/world?abc=zz&query=string", 256 }, 257 }, 258 { 259 // unsorted query string matches exactly 260 Responder: "/hello/world?query=string&query=string2&abc=zz", 261 Paths: []string{ 262 testURL + "hello/world?query=string&query=string2&abc=zz", 263 }, 264 }, 265 // sorted query string matches all cases 266 { 267 Responder: "/hello/world?abc=zz&query=string&query=string2", 268 Paths: []string{ 269 testURL + "hello/world?query=string&query=string2&abc=zz", 270 testURL + "hello/world?query=string2&query=string&abc=zz", 271 testURL + "hello/world?abc=zz&query=string2&query=string", 272 }, 273 }, 274 { 275 Responder: "/hello/world?query", 276 Paths: []string{ 277 testURL + "hello/world?query", 278 }, 279 }, 280 { 281 Responder: "/hello/world?query&abc", 282 Paths: []string{ 283 testURL + "hello/world?query&abc", 284 // testURL + "hello/world?abc&query" won' work as "=" is needed, see below 285 }, 286 }, 287 { 288 // In case the sorting does not matter for received params without 289 // values, we must register params with "=" 290 Responder: "/hello/world?abc=&query=", 291 Paths: []string{ 292 testURL + "hello/world?query&abc", 293 testURL + "hello/world?abc&query", 294 }, 295 }, 296 { 297 Responder: "/hello/world#fragment", 298 Paths: []string{ 299 testURL + "hello/world#fragment", 300 }, 301 }, 302 { 303 Responder: "/hello/world", 304 Paths: []string{ 305 testURL + "hello/world?query=string&abc=zz#fragment", 306 testURL + "hello/world?query=string&abc=zz", 307 testURL + "hello/world#fragment", 308 testURL + "hello/world", 309 }, 310 }, 311 // Regexp cases 312 { 313 Responder: `=~^http://.*/hello/.*ld\z`, 314 Paths: []string{ 315 testURL + "hello/world?query=string&abc=zz#fragment", 316 testURL + "hello/world?query=string&abc=zz", 317 testURL + "hello/world#fragment", 318 testURL + "hello/world", 319 }, 320 }, 321 { 322 Responder: `=~^http://.*/hello/.*ld(\z|[?#])`, 323 Paths: []string{ 324 testURL + "hello/world?query=string&abc=zz#fragment", 325 testURL + "hello/world?query=string&abc=zz", 326 testURL + "hello/world#fragment", 327 testURL + "hello/world", 328 }, 329 }, 330 { 331 Responder: `=~^/hello/.*ld\z`, 332 Paths: []string{ 333 testURL + "hello/world?query=string&abc=zz#fragment", 334 testURL + "hello/world?query=string&abc=zz", 335 testURL + "hello/world#fragment", 336 testURL + "hello/world", 337 }, 338 }, 339 { 340 Responder: `=~^/hello/.*ld(\z|[?#])`, 341 Paths: []string{ 342 testURL + "hello/world?query=string&abc=zz#fragment", 343 testURL + "hello/world?query=string&abc=zz", 344 testURL + "hello/world#fragment", 345 testURL + "hello/world", 346 }, 347 }, 348 { 349 Responder: `=~abc=zz`, 350 Paths: []string{ 351 testURL + "hello/world?query=string&abc=zz#fragment", 352 testURL + "hello/world?query=string&abc=zz", 353 }, 354 }, 355 } { 356 Activate() 357 358 // register the responder 359 RegisterResponder("GET", test.Responder, NewStringResponder(200, "hello world")) 360 361 for _, reqURL := range test.Paths { 362 // make a request for the testURL with a querystring 363 resp, err := http.Get(reqURL) 364 if err != nil { 365 t.Fatalf("%s: expected request %s to succeed", test.Responder, reqURL) 366 } 367 368 data, err := ioutil.ReadAll(resp.Body) 369 if err != nil { 370 t.Fatalf("%s: %s error: %s", test.Responder, reqURL, err) 371 } 372 373 if string(data) != "hello world" { 374 t.Fatalf("%s: expected body of %s to be 'hello world'", test.Responder, reqURL) 375 } 376 } 377 378 DeactivateAndReset() 379 } 380} 381 382type dummyTripper struct{} 383 384func (d *dummyTripper) RoundTrip(*http.Request) (*http.Response, error) { 385 return nil, nil 386} 387 388func TestMockTransportInitialTransport(t *testing.T) { 389 DeactivateAndReset() 390 391 tripper := &dummyTripper{} 392 http.DefaultTransport = tripper 393 394 Activate() 395 396 if http.DefaultTransport == tripper { 397 t.Fatal("expected http.DefaultTransport to be a mock transport") 398 } 399 400 Deactivate() 401 402 if http.DefaultTransport != tripper { 403 t.Fatal("expected http.DefaultTransport to be dummy") 404 } 405} 406 407func TestMockTransportNonDefault(t *testing.T) { 408 // create a custom http client w/ custom Roundtripper 409 client := &http.Client{ 410 Transport: &http.Transport{ 411 Proxy: http.ProxyFromEnvironment, 412 Dial: (&net.Dialer{ 413 Timeout: 60 * time.Second, 414 KeepAlive: 30 * time.Second, 415 }).Dial, 416 TLSHandshakeTimeout: 60 * time.Second, 417 }, 418 } 419 420 // activate mocks for the client 421 ActivateNonDefault(client) 422 defer DeactivateAndReset() 423 424 body := "hello world!" 425 426 RegisterResponder("GET", testURL, NewStringResponder(200, body)) 427 428 req, err := http.NewRequest("GET", testURL, nil) 429 if err != nil { 430 t.Fatal(err) 431 } 432 433 resp, err := client.Do(req) 434 if err != nil { 435 t.Fatal(err) 436 } 437 438 defer resp.Body.Close() 439 440 data, err := ioutil.ReadAll(resp.Body) 441 if err != nil { 442 t.Fatal(err) 443 } 444 445 if string(data) != body { 446 t.FailNow() 447 } 448} 449 450func TestMockTransportRespectsCancel(t *testing.T) { 451 Activate() 452 defer DeactivateAndReset() 453 454 const ( 455 cancelNone = iota 456 cancelReq 457 cancelCtx 458 ) 459 460 cases := []struct { 461 withCancel int 462 cancelNow bool 463 withPanic bool 464 expectedBody string 465 expectedErr error 466 }{ 467 // No cancel specified at all. Falls back to normal behavior 468 {cancelNone, false, false, "hello world", nil}, 469 470 // Cancel returns error 471 {cancelReq, true, false, "", errors.New("request canceled")}, 472 473 // Cancel via context returns error 474 {cancelCtx, true, false, "", errors.New("context canceled")}, 475 476 // Request can be cancelled but it is not cancelled. 477 {cancelReq, false, false, "hello world", nil}, 478 479 // Request can be cancelled but it is not cancelled. 480 {cancelCtx, false, false, "hello world", nil}, 481 482 // Panic in cancelled request is handled 483 {cancelReq, false, true, "", errors.New(`panic in responder: got "oh no"`)}, 484 485 // Panic in cancelled request is handled 486 {cancelCtx, false, true, "", errors.New(`panic in responder: got "oh no"`)}, 487 } 488 489 for _, c := range cases { 490 Reset() 491 if c.withPanic { 492 RegisterResponder("GET", testURL, func(r *http.Request) (*http.Response, error) { 493 time.Sleep(10 * time.Millisecond) 494 panic("oh no") 495 }) 496 } else { 497 RegisterResponder("GET", testURL, func(r *http.Request) (*http.Response, error) { 498 time.Sleep(10 * time.Millisecond) 499 return NewStringResponse(http.StatusOK, "hello world"), nil 500 }) 501 } 502 503 req, err := http.NewRequest("GET", testURL, nil) 504 if err != nil { 505 t.Fatal(err) 506 } 507 508 switch c.withCancel { 509 case cancelReq: 510 cancel := make(chan struct{}, 1) 511 req.Cancel = cancel // nolint: staticcheck 512 if c.cancelNow { 513 cancel <- struct{}{} 514 } 515 case cancelCtx: 516 ctx, cancel := context.WithCancel(req.Context()) 517 req = req.WithContext(ctx) 518 if c.cancelNow { 519 cancel() 520 } else { 521 defer cancel() // avoid ctx leak 522 } 523 } 524 525 resp, err := http.DefaultClient.Do(req) 526 527 // If we expect an error but none was returned, it's fatal for this test... 528 if err == nil && c.expectedErr != nil { 529 t.Fatal("Error should not be nil") 530 } 531 532 if err != nil { 533 got := err.(*url.Error) 534 // Do not use reflect.DeepEqual as go 1.13 includes stack frames 535 // into errors issued by errors.New() 536 if c.expectedErr == nil || got.Err.Error() != c.expectedErr.Error() { 537 t.Errorf("Expected error: %v, got: %v", c.expectedErr, got.Err) 538 } 539 } 540 541 if c.expectedBody != "" { 542 assertBody(t, resp, c.expectedBody) 543 } 544 } 545} 546 547func TestMockTransportRespectsTimeout(t *testing.T) { 548 timeout := time.Millisecond 549 client := &http.Client{ 550 Timeout: timeout, 551 } 552 553 ActivateNonDefault(client) 554 defer DeactivateAndReset() 555 556 RegisterResponder( 557 "GET", testURL, 558 func(r *http.Request) (*http.Response, error) { 559 time.Sleep(100 * timeout) 560 return NewStringResponse(http.StatusOK, ""), nil 561 }, 562 ) 563 564 _, err := client.Get(testURL) 565 if err == nil { 566 t.Fail() 567 } 568} 569 570func TestMockTransportCallCount(t *testing.T) { 571 Reset() 572 Activate() 573 defer Deactivate() 574 575 const ( 576 url = "https://github.com/path?b=1&a=2" 577 url2 = "https://gitlab.com/" 578 ) 579 580 RegisterResponder("GET", url, NewStringResponder(200, "body")) 581 RegisterResponder("POST", "=~gitlab", NewStringResponder(200, "body")) 582 583 _, err := http.Get(url) 584 if err != nil { 585 t.Fatal(err) 586 } 587 588 buff := new(bytes.Buffer) 589 json.NewEncoder(buff).Encode("{}") // nolint: errcheck 590 _, err = http.Post(url2, "application/json", buff) 591 if err != nil { 592 t.Fatal(err) 593 } 594 595 _, err = http.Get(url) 596 if err != nil { 597 t.Fatal(err) 598 } 599 600 totalCallCount := GetTotalCallCount() 601 if totalCallCount != 3 { 602 t.Fatalf("did not track the total count of calls correctly. expected it to be 3, but it was %v", totalCallCount) 603 } 604 605 info := GetCallCountInfo() 606 expectedInfo := map[string]int{ 607 "GET " + url: 2, 608 // Regexp match generates 2 entries: 609 "POST " + url2: 1, // the matched call 610 "POST =~gitlab": 1, // the regexp responder 611 } 612 613 if !reflect.DeepEqual(info, expectedInfo) { 614 t.Fatalf("did not correctly track the call count info. expected it to be \n %+v \n but it was \n %+v \n", expectedInfo, info) 615 } 616 617 Reset() 618 619 afterResetTotalCallCount := GetTotalCallCount() 620 if afterResetTotalCallCount != 0 { 621 t.Fatalf("did not reset the total count of calls correctly. expected it to be 0 after reset, but it was %v", afterResetTotalCallCount) 622 } 623} 624 625func TestRegisterResponderWithQuery(t *testing.T) { 626 // Just in case a panic occurs 627 defer DeactivateAndReset() 628 629 // create a custom http client w/ custom Roundtripper 630 client := &http.Client{ 631 Transport: &http.Transport{ 632 Proxy: http.ProxyFromEnvironment, 633 Dial: (&net.Dialer{ 634 Timeout: 60 * time.Second, 635 KeepAlive: 30 * time.Second, 636 }).Dial, 637 TLSHandshakeTimeout: 60 * time.Second, 638 }, 639 } 640 641 body := "hello world!" 642 testURLPath := "http://acme.test/api" 643 644 for _, test := range []struct { 645 URL string 646 Queries []interface{} 647 URLs []string 648 }{ 649 { 650 Queries: []interface{}{ 651 map[string]string{"a": "1", "b": "2"}, 652 "a=1&b=2", 653 "b=2&a=1", 654 url.Values{"a": []string{"1"}, "b": []string{"2"}}, 655 }, 656 URLs: []string{ 657 "http://acme.test/api?a=1&b=2", 658 "http://acme.test/api?b=2&a=1", 659 }, 660 }, 661 { 662 Queries: []interface{}{ 663 url.Values{ 664 "a": []string{"3", "2", "1"}, 665 "b": []string{"4", "2"}, 666 "c": []string{""}, // is the net/url way to record params without values 667 // Test: 668 // u, _ := url.Parse("/hello/world?query") 669 // fmt.Printf("%d<%s>\n", len(u.Query()["query"]), u.Query()["query"][0]) 670 // // prints "1<>" 671 }, 672 "a=1&b=2&a=3&c&b=4&a=2", 673 "b=2&a=1&c=&b=4&a=2&a=3", 674 nil, 675 }, 676 URLs: []string{ 677 testURLPath + "?a=1&b=2&a=3&c&b=4&a=2", 678 testURLPath + "?a=1&b=2&a=3&c=&b=4&a=2", 679 testURLPath + "?b=2&a=1&c=&b=4&a=2&a=3", 680 testURLPath + "?b=2&a=1&c&b=4&a=2&a=3", 681 }, 682 }, 683 } { 684 for _, query := range test.Queries { 685 ActivateNonDefault(client) 686 RegisterResponderWithQuery("GET", testURLPath, query, NewStringResponder(200, body)) 687 688 for _, url := range test.URLs { 689 req, err := http.NewRequest("GET", url, nil) 690 if err != nil { 691 t.Fatal(err) 692 } 693 resp, err := client.Do(req) 694 if err != nil { 695 t.Fatal(err) 696 } 697 data, err := ioutil.ReadAll(resp.Body) 698 resp.Body.Close() 699 if err != nil { 700 t.Fatal(err) 701 } 702 if string(data) != body { 703 t.Fatalf("query=%v URL=%s: %s ≠ %s", query, url, string(data), body) 704 } 705 } 706 707 DeactivateAndReset() 708 } 709 } 710} 711 712func TestRegisterResponderWithQueryPanic(t *testing.T) { 713 resp := NewStringResponder(200, "hello world!") 714 715 for _, test := range []struct { 716 Path string 717 Query interface{} 718 PanicPrefix string 719 }{ 720 { 721 Path: "foobar", 722 Query: "%", 723 PanicPrefix: "RegisterResponderWithQuery bad query string: ", 724 }, 725 { 726 Path: "foobar", 727 Query: 1234, 728 PanicPrefix: "RegisterResponderWithQuery bad query type int. Only url.Values, map[string]string and string are allowed", 729 }, 730 { 731 Path: `=~regexp.*\z`, 732 Query: "", 733 PanicPrefix: `path begins with "=~", RegisterResponder should be used instead of RegisterResponderWithQuery`, 734 }, 735 } { 736 var ( 737 didntPanic bool 738 panicVal interface{} 739 ) 740 func() { 741 defer func() { 742 panicVal = recover() 743 }() 744 745 RegisterResponderWithQuery("GET", test.Path, test.Query, resp) 746 didntPanic = true 747 }() 748 749 if didntPanic { 750 t.Fatalf("RegisterResponderWithQuery + query=%v did not panic", test.Query) 751 } 752 753 panicStr, ok := panicVal.(string) 754 if !ok || !strings.HasPrefix(panicStr, test.PanicPrefix) { 755 t.Fatalf(`RegisterResponderWithQuery + query=%v panic="%v" expected prefix="%v"`, 756 test.Query, panicVal, test.PanicPrefix) 757 } 758 } 759} 760 761func TestRegisterRegexpResponder(t *testing.T) { 762 Activate() 763 defer DeactivateAndReset() 764 765 rx := regexp.MustCompile("ex.mple") 766 767 RegisterRegexpResponder("GET", rx, NewStringResponder(200, "first")) 768 // Overwrite responder 769 RegisterRegexpResponder("GET", rx, NewStringResponder(200, "second")) 770 771 resp, err := http.Get(testURL) 772 if err != nil { 773 t.Fatalf("expected request %s to succeed", testURL) 774 } 775 776 data, err := ioutil.ReadAll(resp.Body) 777 if err != nil { 778 t.Fatalf("%s error: %s", testURL, err) 779 } 780 781 if string(data) != "second" { 782 t.Fatalf("expected body of %s to be 'hello world'", testURL) 783 } 784} 785 786func TestSubmatches(t *testing.T) { 787 req, err := http.NewRequest("GET", "/foo/bar", nil) 788 if err != nil { 789 t.Fatal(err) 790 } 791 792 var req2 *http.Request 793 794 t.Run("setSubmatches", func(t *testing.T) { 795 req2 = setSubmatches(req, nil) 796 if req2 != req { 797 t.Error("setSubmatches(req, nil) should return the same request") 798 } 799 800 req2 = setSubmatches(req, []string{}) 801 if req2 != req { 802 t.Error("setSubmatches(req, []string{}) should return the same request") 803 } 804 805 req2 = setSubmatches(req, []string{"foo", "123", "-123", "12.3"}) 806 if req2 == req { 807 t.Error("setSubmatches(req, []string{...}) should NOT return the same request") 808 } 809 }) 810 811 t.Run("GetSubmatch", func(t *testing.T) { 812 _, err := GetSubmatch(req, 1) 813 if err != ErrSubmatchNotFound { 814 t.Errorf("Submatch should not be found in req: %v", err) 815 } 816 817 _, err = GetSubmatch(req2, 5) 818 if err != ErrSubmatchNotFound { 819 t.Errorf("Submatch #5 should not be found in req2: %v", err) 820 } 821 822 s, err := GetSubmatch(req2, 1) 823 if err != nil { 824 t.Errorf("GetSubmatch(req2, 1) failed: %v", err) 825 } 826 if s != "foo" { 827 t.Errorf("GetSubmatch(req2, 1) failed, got: %v, expected: foo", s) 828 } 829 830 s, err = GetSubmatch(req2, 4) 831 if err != nil { 832 t.Errorf("GetSubmatch(req2, 4) failed: %v", err) 833 } 834 if s != "12.3" { 835 t.Errorf("GetSubmatch(req2, 4) failed, got: %v, expected: 12.3", s) 836 } 837 838 s = MustGetSubmatch(req2, 4) 839 if s != "12.3" { 840 t.Errorf("GetSubmatch(req2, 4) failed, got: %v, expected: 12.3", s) 841 } 842 }) 843 844 t.Run("GetSubmatchAsInt", func(t *testing.T) { 845 _, err := GetSubmatchAsInt(req, 1) 846 if err != ErrSubmatchNotFound { 847 t.Errorf("Submatch should not be found in req: %v", err) 848 } 849 850 _, err = GetSubmatchAsInt(req2, 4) // not an int 851 if err == nil || err == ErrSubmatchNotFound { 852 t.Errorf("Submatch should not be an int64: %v", err) 853 } 854 855 i, err := GetSubmatchAsInt(req2, 3) 856 if err != nil { 857 t.Errorf("GetSubmatchAsInt(req2, 3) failed: %v", err) 858 } 859 if i != -123 { 860 t.Errorf("GetSubmatchAsInt(req2, 3) failed, got: %d, expected: -123", i) 861 } 862 863 i = MustGetSubmatchAsInt(req2, 3) 864 if i != -123 { 865 t.Errorf("MustGetSubmatchAsInt(req2, 3) failed, got: %d, expected: -123", i) 866 } 867 }) 868 869 t.Run("GetSubmatchAsUint", func(t *testing.T) { 870 _, err := GetSubmatchAsUint(req, 1) 871 if err != ErrSubmatchNotFound { 872 t.Errorf("Submatch should not be found in req: %v", err) 873 } 874 875 _, err = GetSubmatchAsUint(req2, 3) // not a uint 876 if err == nil || err == ErrSubmatchNotFound { 877 t.Errorf("Submatch should not be an uint64: %v", err) 878 } 879 880 u, err := GetSubmatchAsUint(req2, 2) 881 if err != nil { 882 t.Errorf("GetSubmatchAsUint(req2, 2) failed: %v", err) 883 } 884 if u != 123 { 885 t.Errorf("GetSubmatchAsUint(req2, 2) failed, got: %d, expected: 123", u) 886 } 887 888 u = MustGetSubmatchAsUint(req2, 2) 889 if u != 123 { 890 t.Errorf("MustGetSubmatchAsUint(req2, 2) failed, got: %d, expected: 123", u) 891 } 892 }) 893 894 t.Run("GetSubmatchAsFloat", func(t *testing.T) { 895 _, err := GetSubmatchAsFloat(req, 1) 896 if err != ErrSubmatchNotFound { 897 t.Errorf("Submatch should not be found in req: %v", err) 898 } 899 900 _, err = GetSubmatchAsFloat(req2, 1) // not a float 901 if err == nil || err == ErrSubmatchNotFound { 902 t.Errorf("Submatch should not be an float64: %v", err) 903 } 904 905 f, err := GetSubmatchAsFloat(req2, 4) 906 if err != nil { 907 t.Errorf("GetSubmatchAsFloat(req2, 4) failed: %v", err) 908 } 909 if f != 12.3 { 910 t.Errorf("GetSubmatchAsFloat(req2, 4) failed, got: %f, expected: 12.3", f) 911 } 912 913 f = MustGetSubmatchAsFloat(req2, 4) 914 if f != 12.3 { 915 t.Errorf("MustGetSubmatchAsFloat(req2, 4) failed, got: %f, expected: 12.3", f) 916 } 917 }) 918 919 t.Run("GetSubmatch* panics", func(t *testing.T) { 920 for _, test := range []struct { 921 Name string 922 Fn func() 923 PanicPrefix string 924 }{ 925 { 926 Name: "GetSubmatch & n < 1", 927 Fn: func() { GetSubmatch(req, 0) }, // nolint: errcheck 928 PanicPrefix: "getting submatches starts at 1, not 0", 929 }, 930 { 931 Name: "MustGetSubmatch", 932 Fn: func() { MustGetSubmatch(req, 1) }, 933 PanicPrefix: "GetSubmatch failed: " + ErrSubmatchNotFound.Error(), 934 }, 935 { 936 Name: "MustGetSubmatchAsInt", 937 Fn: func() { MustGetSubmatchAsInt(req2, 4) }, // not an int 938 PanicPrefix: "GetSubmatchAsInt failed: ", 939 }, 940 { 941 Name: "MustGetSubmatchAsUint", 942 Fn: func() { MustGetSubmatchAsUint(req2, 3) }, // not a uint 943 PanicPrefix: "GetSubmatchAsUint failed: ", 944 }, 945 { 946 Name: "GetSubmatchAsFloat", 947 Fn: func() { MustGetSubmatchAsFloat(req2, 1) }, // not a float 948 PanicPrefix: "GetSubmatchAsFloat failed: ", 949 }, 950 } { 951 var ( 952 didntPanic bool 953 panicVal interface{} 954 ) 955 func() { 956 defer func() { panicVal = recover() }() 957 test.Fn() 958 didntPanic = true 959 }() 960 961 if didntPanic { 962 t.Errorf("%s did not panic", test.Name) 963 } 964 965 panicStr, ok := panicVal.(string) 966 if !ok || !strings.HasPrefix(panicStr, test.PanicPrefix) { 967 t.Errorf(`%s panic="%v" expected prefix="%v"`, test.Name, panicVal, test.PanicPrefix) 968 } 969 } 970 }) 971 972 t.Run("Full test", func(t *testing.T) { 973 Activate() 974 defer DeactivateAndReset() 975 976 var ( 977 id uint64 978 delta float64 979 deltaStr string 980 inc int64 981 ) 982 RegisterResponder("GET", `=~^/id/(\d+)\?delta=(\d+(?:\.\d*)?)&inc=(-?\d+)\z`, 983 func(req *http.Request) (*http.Response, error) { 984 id = MustGetSubmatchAsUint(req, 1) 985 delta = MustGetSubmatchAsFloat(req, 2) 986 deltaStr = MustGetSubmatch(req, 2) 987 inc = MustGetSubmatchAsInt(req, 3) 988 989 return NewStringResponse(http.StatusOK, "OK"), nil 990 }) 991 992 resp, err := http.Get("http://example.tld/id/123?delta=1.2&inc=-5") 993 if err != nil { 994 t.Fatal(err) 995 } 996 assertBody(t, resp, "OK") 997 998 // Check submatches 999 if id != 123 { 1000 t.Errorf("seems MustGetSubmatchAsUint failed, got: %d, expected: 123", id) 1001 } 1002 if delta != 1.2 { 1003 t.Errorf("seems MustGetSubmatchAsFloat failed, got: %f, expected: 1.2", delta) 1004 } 1005 if deltaStr != "1.2" { 1006 t.Errorf("seems MustGetSubmatch failed, got: %v, expected: 1.2", deltaStr) 1007 } 1008 if inc != -5 { 1009 t.Errorf("seems MustGetSubmatchAsInt failed, got: %d, expected: 123", inc) 1010 } 1011 }) 1012} 1013 1014func TestCheckStackTracer(t *testing.T) { 1015 req, err := http.NewRequest("GET", "http://foo.bar/", nil) 1016 if err != nil { 1017 t.Fatal(err) 1018 } 1019 1020 // no error 1021 gotErr := checkStackTracer(req, nil) 1022 if gotErr != nil { 1023 t.Errorf(`checkStackTracer(nil) should return nil, not %v`, gotErr) 1024 } 1025 1026 // Classic error 1027 err = errors.New("error") 1028 gotErr = checkStackTracer(req, err) 1029 if err != gotErr { 1030 t.Errorf(`checkStackTracer(err) should return %v, not %v`, err, gotErr) 1031 } 1032 1033 // stackTracer without customFn 1034 origErr := errors.New("foo") 1035 errTracer := stackTracer{ 1036 err: origErr, 1037 } 1038 gotErr = checkStackTracer(req, errTracer) 1039 if gotErr != origErr { 1040 t.Errorf(`Returned error mismatch, expected: %v, got: %v`, origErr, gotErr) 1041 } 1042 1043 // stackTracer with nil error & without customFn 1044 errTracer = stackTracer{} 1045 gotErr = checkStackTracer(req, errTracer) 1046 if gotErr != nil { 1047 t.Errorf(`Returned error mismatch, expected: nil, got: %v`, gotErr) 1048 } 1049 1050 // stackTracer 1051 var mesg string 1052 errTracer = stackTracer{ 1053 err: origErr, 1054 customFn: func(args ...interface{}) { 1055 mesg = args[0].(string) 1056 }, 1057 } 1058 gotErr = checkStackTracer(req, errTracer) 1059 if !strings.HasPrefix(mesg, "foo\nCalled from ") || strings.HasSuffix(mesg, "\n") { 1060 t.Errorf(`mesg does not match "^foo\nCalled from .*[^\n]\z", it is "` + mesg + `"`) 1061 } 1062 if gotErr != origErr { 1063 t.Errorf(`Returned error mismatch, expected: %v, got: %v`, origErr, gotErr) 1064 } 1065 1066 // stackTracer with nil error but customFn 1067 mesg = "" 1068 errTracer = stackTracer{ 1069 customFn: func(args ...interface{}) { 1070 mesg = args[0].(string) 1071 }, 1072 } 1073 gotErr = checkStackTracer(req, errTracer) 1074 if !strings.HasPrefix(mesg, "GET http://foo.bar/\nCalled from ") || strings.HasSuffix(mesg, "\n") { 1075 t.Errorf(`mesg does not match "^foo\nCalled from .*[^\n]\z", it is "` + mesg + `"`) 1076 } 1077 if gotErr != nil { 1078 t.Errorf(`Returned error mismatch, expected: nil, got: %v`, gotErr) 1079 } 1080 1081 // Full test using Trace() Responder 1082 Activate() 1083 defer Deactivate() 1084 1085 const url = "https://foo.bar/" 1086 mesg = "" 1087 RegisterResponder("GET", url, 1088 NewStringResponder(200, "{}"). 1089 Trace(func(args ...interface{}) { mesg = args[0].(string) })) 1090 1091 resp, err := http.Get(url) 1092 if err != nil { 1093 t.Fatal(err) 1094 } 1095 defer resp.Body.Close() 1096 1097 data, err := ioutil.ReadAll(resp.Body) 1098 if err != nil { 1099 t.Fatal(err) 1100 } 1101 1102 if string(data) != "{}" { 1103 t.FailNow() 1104 } 1105 1106 // Check that first frame is the net/http.Get() call 1107 if !strings.HasPrefix(mesg, "GET https://foo.bar/\nCalled from net/http.Get()\n at ") || 1108 strings.HasSuffix(mesg, "\n") { 1109 t.Errorf("Bad mesg: <%v>", mesg) 1110 } 1111} 1112