1 package httpmock 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/url" 10 "regexp" 11 "sort" 12 "strconv" 13 "strings" 14 "sync" 15 16 "github.com/jarcoal/httpmock/internal" 17 ) 18 19 const regexpPrefix = "=~" 20 21 // NoResponderFound is returned when no responders are found for a 22 // given HTTP method and URL. 23 var NoResponderFound = internal.NoResponderFound 24 25 var stdMethods = map[string]bool{ 26 "CONNECT": true, // Section 9.9 27 "DELETE": true, // Section 9.7 28 "GET": true, // Section 9.3 29 "HEAD": true, // Section 9.4 30 "OPTIONS": true, // Section 9.2 31 "POST": true, // Section 9.5 32 "PUT": true, // Section 9.6 33 "TRACE": true, // Section 9.8 34 } 35 36 // methodProbablyWrong returns true if method has probably wrong case. 37 func methodProbablyWrong(method string) bool { 38 return !stdMethods[method] && stdMethods[strings.ToUpper(method)] 39 } 40 41 // ConnectionFailure is a responder that returns a connection failure. 42 // This is the default responder, and is called when no other matching 43 // responder is found. 44 func ConnectionFailure(*http.Request) (*http.Response, error) { 45 return nil, NoResponderFound 46 } 47 48 // NewMockTransport creates a new *MockTransport with no responders. 49 func NewMockTransport() *MockTransport { 50 return &MockTransport{ 51 responders: make(map[internal.RouteKey]Responder), 52 callCountInfo: make(map[internal.RouteKey]int), 53 } 54 } 55 56 type regexpResponder struct { 57 origRx string 58 method string 59 rx *regexp.Regexp 60 responder Responder 61 } 62 63 // MockTransport implements http.RoundTripper, which fulfills single 64 // http requests issued by an http.Client. This implementation 65 // doesn't actually make the call, instead deferring to the registered 66 // list of responders. 67 type MockTransport struct { 68 // DontCheckMethod disables standard methods check. By default, if 69 // a responder is registered using a lower-cased method among CONNECT, 70 // DELETE, GET, HEAD, OPTIONS, POST, PUT and TRACE, a panic occurs 71 // as it is probably a mistake. 72 DontCheckMethod bool 73 mu sync.RWMutex 74 responders map[internal.RouteKey]Responder 75 regexpResponders []regexpResponder 76 noResponder Responder 77 callCountInfo map[internal.RouteKey]int 78 totalCallCount int 79 } 80 81 func (m *MockTransport) findResponder(method string, url *url.URL) ( 82 responder Responder, 83 key, respKey internal.RouteKey, 84 submatches []string, 85 ) { 86 urlStr := url.String() 87 key = internal.RouteKey{ 88 Method: method, 89 } 90 for _, getResponder := range []func(internal.RouteKey) (Responder, internal.RouteKey, []string){ 91 m.responderForKey, // Exact match 92 m.regexpResponderForKey, // Regexp match 93 } { 94 // try and get a responder that matches the method and URL with 95 // query params untouched: http://z.tld/path?q... 96 key.URL = urlStr 97 responder, respKey, submatches = getResponder(key) 98 if responder != nil { 99 break 100 } 101 102 // if we weren't able to find a responder, try with the URL *and* 103 // sorted query params 104 query := sortedQuery(url.Query()) 105 if query != "" { 106 // Replace unsorted query params by sorted ones: 107 // http://z.tld/path?sorted_q... 108 key.URL = strings.Replace(urlStr, url.RawQuery, query, 1) 109 responder, respKey, submatches = getResponder(key) 110 if responder != nil { 111 break 112 } 113 } 114 115 // if we weren't able to find a responder, try without any query params 116 strippedURL := *url 117 strippedURL.RawQuery = "" 118 strippedURL.Fragment = "" 119 120 // go1.6 does not handle URL.ForceQuery, so in case it is set in go>1.6, 121 // remove the "?" manually if present. 122 surl := strings.TrimSuffix(strippedURL.String(), "?") 123 124 hasQueryString := urlStr != surl 125 126 // if the URL contains a querystring then we strip off the 127 // querystring and try again: http://z.tld/path 128 if hasQueryString { 129 key.URL = surl 130 responder, respKey, submatches = getResponder(key) 131 if responder != nil { 132 break 133 } 134 } 135 136 // if we weren't able to find a responder for the full URL, try with 137 // the path part only 138 pathAlone := url.RawPath 139 if pathAlone == "" { 140 pathAlone = url.Path 141 } 142 143 // First with unsorted querystring: /path?q... 144 if hasQueryString { 145 key.URL = pathAlone + strings.TrimPrefix(urlStr, surl) // concat after-path part 146 responder, respKey, submatches = getResponder(key) 147 if responder != nil { 148 break 149 } 150 151 // Then with sorted querystring: /path?sorted_q... 152 key.URL = pathAlone + "?" + sortedQuery(url.Query()) 153 if url.Fragment != "" { 154 key.URL += "#" + url.Fragment 155 } 156 responder, respKey, submatches = getResponder(key) 157 if responder != nil { 158 break 159 } 160 } 161 162 // Then using path alone: /path 163 key.URL = pathAlone 164 responder, respKey, submatches = getResponder(key) 165 if responder != nil { 166 break 167 } 168 } 169 return 170 } 171 172 // RoundTrip receives HTTP requests and routes them to the appropriate 173 // responder. It is required to implement the http.RoundTripper 174 // interface. You will not interact with this directly, instead the 175 // *http.Client you are using will call it for you. 176 func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { 177 method := req.Method 178 if method == "" { 179 // http.Request.Method is documented to default to GET: 180 method = http.MethodGet 181 } 182 183 var suggested *internal.ErrorNoResponderFoundMistake 184 185 responder, key, respKey, submatches := m.findResponder(method, req.URL) 186 if responder == nil { 187 // Responder not found, try to detect some common user mistakes on 188 // method then on path 189 var altResp Responder 190 var altKey internal.RouteKey 191 192 // On method first 193 if methodProbablyWrong(method) { 194 // Get → GET 195 altResp, _, altKey, _ = m.findResponder(strings.ToUpper(method), req.URL) 196 } 197 if altResp == nil { 198 // Search for any other method 199 altResp, _, altKey, _ = m.findResponder("", req.URL) 200 } 201 if altResp != nil { 202 suggested = &internal.ErrorNoResponderFoundMistake{ 203 Kind: "method", 204 Orig: method, 205 Suggested: altKey.Method, 206 } 207 } else { 208 // Then on path 209 if altResp == nil && strings.HasSuffix(req.URL.Path, "/") { 210 // Try without final "/" 211 u := *req.URL 212 u.Path = strings.TrimSuffix(u.Path, "/") 213 altResp, _, altKey, _ = m.findResponder("", &u) 214 } 215 if altResp == nil && strings.Contains(req.URL.Path, "//") { 216 // Try without double "/" 217 u := *req.URL 218 squash := false 219 u.Path = strings.Map(func(r rune) rune { 220 if r == '/' { 221 if squash { 222 return -1 223 } 224 squash = true 225 } else { 226 squash = false 227 } 228 return r 229 }, u.Path) 230 altResp, _, altKey, _ = m.findResponder("", &u) 231 } 232 if altResp != nil { 233 suggested = &internal.ErrorNoResponderFoundMistake{ 234 Kind: "URL", 235 Orig: req.URL.String(), 236 Suggested: altKey.URL, 237 } 238 } 239 } 240 } 241 242 m.mu.Lock() 243 // if we found a responder, call it 244 if responder != nil { 245 m.callCountInfo[key]++ 246 if key != respKey { 247 m.callCountInfo[respKey]++ 248 } 249 m.totalCallCount++ 250 } else if m.noResponder != nil { 251 // we didn't find a responder, so fire the 'no responder' responder 252 m.callCountInfo[internal.NoResponder]++ 253 m.totalCallCount++ 254 255 // give a hint to NewNotFoundResponder() if it is a possible 256 // method or URL error 257 if suggested != nil { 258 req = req.WithContext(context.WithValue(req.Context(), suggestedKey, &suggestedInfo{ 259 kind: suggested.Kind, 260 suggested: suggested.Suggested, 261 })) 262 } 263 responder = m.noResponder 264 } 265 m.mu.Unlock() 266 267 if responder == nil { 268 if suggested != nil { 269 return nil, suggested 270 } 271 return ConnectionFailure(req) 272 } 273 return runCancelable(responder, internal.SetSubmatches(req, submatches)) 274 } 275 276 // NumResponders returns the number of responders currently in use. 277 // The responder registered with RegisterNoResponder() is not taken 278 // into account. 279 func (m *MockTransport) NumResponders() int { 280 m.mu.RLock() 281 defer m.mu.RUnlock() 282 return len(m.responders) + len(m.regexpResponders) 283 } 284 285 // Responders returns the list of currently registered responders. 286 // Each responder is listed as a string containing "METHOD URL". 287 // Non-regexp responders are listed first in alphabetical order 288 // (sorted by URL then METHOD), then regexp responders in the order 289 // they have been registered. 290 // The responder registered with RegisterNoResponder() is not listed. 291 func (m *MockTransport) Responders() []string { 292 m.mu.RLock() 293 defer m.mu.RUnlock() 294 295 rks := make([]internal.RouteKey, 0, len(m.responders)) 296 for rk := range m.responders { 297 rks = append(rks, rk) 298 } 299 sort.Slice(rks, func(i, j int) bool { 300 if rks[i].URL == rks[j].URL { 301 return rks[i].Method < rks[j].Method 302 } 303 return rks[i].URL < rks[j].URL 304 }) 305 306 rs := make([]string, 0, len(m.responders)+len(m.regexpResponders)) 307 for _, rk := range rks { 308 rs = append(rs, rk.String()) 309 } 310 for _, rr := range m.regexpResponders { 311 rs = append(rs, rr.method+" "+rr.origRx) 312 } 313 return rs 314 } 315 316 func runCancelable(responder Responder, req *http.Request) (*http.Response, error) { 317 ctx := req.Context() 318 if req.Cancel == nil && ctx.Done() == nil { // nolint: staticcheck 319 resp, err := responder(req) 320 return resp, internal.CheckStackTracer(req, err) 321 } 322 323 // Set up a goroutine that translates a close(req.Cancel) into a 324 // "request canceled" error, and another one that runs the 325 // responder. Then race them: first to the result channel wins. 326 327 type result struct { 328 response *http.Response 329 err error 330 } 331 resultch := make(chan result, 1) 332 done := make(chan struct{}, 1) 333 334 go func() { 335 select { 336 case <-req.Cancel: // nolint: staticcheck 337 resultch <- result{ 338 response: nil, 339 err: errors.New("request canceled"), 340 } 341 case <-ctx.Done(): 342 resultch <- result{ 343 response: nil, 344 err: ctx.Err(), 345 } 346 case <-done: 347 } 348 }() 349 350 go func() { 351 defer func() { 352 if err := recover(); err != nil { 353 resultch <- result{ 354 response: nil, 355 err: fmt.Errorf("panic in responder: got %q", err), 356 } 357 } 358 }() 359 360 response, err := responder(req) 361 resultch <- result{ 362 response: response, 363 err: err, 364 } 365 }() 366 367 r := <-resultch 368 369 // if a cancel() issued from context.WithCancel() or a 370 // close(req.Cancel) are never coming, we'll need to unblock the 371 // first goroutine. 372 done <- struct{}{} 373 374 return r.response, internal.CheckStackTracer(req, r.err) 375 } 376 377 // responderForKey returns a responder for a given key. 378 func (m *MockTransport) responderForKey(key internal.RouteKey) (Responder, internal.RouteKey, []string) { 379 m.mu.RLock() 380 defer m.mu.RUnlock() 381 if key.Method != "" { 382 return m.responders[key], key, nil 383 } 384 385 for k, resp := range m.responders { 386 if key.URL == k.URL { 387 return resp, k, nil 388 } 389 } 390 return nil, key, nil 391 } 392 393 // responderForKeyUsingRegexp returns the first responder matching a 394 // given key using regexps. 395 func (m *MockTransport) regexpResponderForKey(key internal.RouteKey) (Responder, internal.RouteKey, []string) { 396 m.mu.RLock() 397 defer m.mu.RUnlock() 398 for _, regInfo := range m.regexpResponders { 399 if key.Method == "" || regInfo.method == key.Method { 400 if sm := regInfo.rx.FindStringSubmatch(key.URL); sm != nil { 401 if len(sm) == 1 { 402 sm = nil 403 } else { 404 sm = sm[1:] 405 } 406 return regInfo.responder, internal.RouteKey{ 407 Method: regInfo.method, 408 URL: regInfo.origRx, 409 }, sm 410 } 411 } 412 } 413 return nil, key, nil 414 } 415 416 func isRegexpURL(url string) bool { 417 return strings.HasPrefix(url, regexpPrefix) 418 } 419 420 func (m *MockTransport) checkMethod(method string) { 421 if !m.DontCheckMethod && methodProbablyWrong(method) { 422 panic(fmt.Sprintf("You probably want to use method %q instead of %q? If not and so want to disable this check, set MockTransport.DontCheckMethod field to true", 423 strings.ToUpper(method), 424 method, 425 )) 426 } 427 } 428 429 // RegisterResponder adds a new responder, associated with a given 430 // HTTP method and URL (or path). 431 // 432 // When a request comes in that matches, the responder is called and 433 // the response returned to the client. 434 // 435 // If url contains query parameters, their order matters as well as 436 // their content. All following URLs are here considered as different: 437 // http://z.tld?a=1&b=1 438 // http://z.tld?b=1&a=1 439 // http://z.tld?a&b 440 // http://z.tld?a=&b= 441 // 442 // If url begins with "=~", the following chars are considered as a 443 // regular expression. If this regexp can not be compiled, it panics. 444 // Note that the "=~" prefix remains in statistics returned by 445 // GetCallCountInfo(). As 2 regexps can match the same URL, the regexp 446 // responders are tested in the order they are registered. Registering 447 // an already existing regexp responder (same method & same regexp 448 // string) replaces its responder, but does not change its position. 449 // 450 // Registering an already existing responder resets the corresponding 451 // statistics as returned by GetCallCountInfo(). 452 // 453 // Registering a nil Responder removes the existing one and the 454 // corresponding statistics as returned by GetCallCountInfo(). It does 455 // nothing if it does not already exist. 456 // 457 // See RegisterRegexpResponder() to directly pass a *regexp.Regexp. 458 // 459 // Example: 460 // func TestFetchArticles(t *testing.T) { 461 // httpmock.Activate() 462 // defer httpmock.DeactivateAndReset() 463 // 464 // httpmock.RegisterResponder("GET", "http://example.com/", 465 // httpmock.NewStringResponder(200, "hello world")) 466 // 467 // httpmock.RegisterResponder("GET", "/path/only", 468 // httpmock.NewStringResponder("any host hello world", 200)) 469 // 470 // httpmock.RegisterResponder("GET", `=~^/item/id/\d+\z`, 471 // httpmock.NewStringResponder("any item get", 200)) 472 // 473 // // requests to http://example.com/ now return "hello world" and 474 // // requests to any host with path /path/only return "any host hello world" 475 // // requests to any host with path matching ^/item/id/\d+\z regular expression return "any item get" 476 // } 477 // 478 // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, 479 // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible 480 // mistake. This panic can be disabled by setting m.DontCheckMethod to 481 // true prior to this call. 482 func (m *MockTransport) RegisterResponder(method, url string, responder Responder) { 483 m.checkMethod(method) 484 485 if isRegexpURL(url) { 486 m.registerRegexpResponder(regexpResponder{ 487 origRx: url, 488 method: method, 489 rx: regexp.MustCompile(url[2:]), 490 responder: responder, 491 }) 492 return 493 } 494 495 key := internal.RouteKey{ 496 Method: method, 497 URL: url, 498 } 499 500 m.mu.Lock() 501 if responder == nil { 502 delete(m.responders, key) 503 delete(m.callCountInfo, key) 504 } else { 505 m.responders[key] = responder 506 m.callCountInfo[key] = 0 507 } 508 m.mu.Unlock() 509 } 510 511 func (m *MockTransport) registerRegexpResponder(rxResp regexpResponder) { 512 m.mu.Lock() 513 defer m.mu.Unlock() 514 515 found: 516 for { 517 for i, rr := range m.regexpResponders { 518 if rr.method == rxResp.method && rr.origRx == rxResp.origRx { 519 if rxResp.responder == nil { 520 copy(m.regexpResponders[:i], m.regexpResponders[i+1:]) 521 m.regexpResponders[len(m.regexpResponders)-1] = regexpResponder{} 522 m.regexpResponders = m.regexpResponders[:len(m.regexpResponders)-1] 523 } else { 524 m.regexpResponders[i] = rxResp 525 } 526 break found 527 } 528 } 529 if rxResp.responder != nil { 530 m.regexpResponders = append(m.regexpResponders, rxResp) 531 } 532 break // nolint: staticcheck 533 } 534 535 key := internal.RouteKey{ 536 Method: rxResp.method, 537 URL: rxResp.origRx, 538 } 539 if rxResp.responder == nil { 540 delete(m.callCountInfo, key) 541 } else { 542 m.callCountInfo[key] = 0 543 } 544 } 545 546 // RegisterRegexpResponder adds a new responder, associated with a given 547 // HTTP method and URL (or path) regular expression. 548 // 549 // When a request comes in that matches, the responder is called and 550 // the response returned to the client. 551 // 552 // As 2 regexps can match the same URL, the regexp responders are 553 // tested in the order they are registered. Registering an already 554 // existing regexp responder (same method & same regexp string) 555 // replaces its responder, but does not change its position, and 556 // resets the corresponding statistics as returned by GetCallCountInfo(). 557 // 558 // Registering a nil Responder removes the existing one and the 559 // corresponding statistics as returned by GetCallCountInfo(). It does 560 // nothing if it does not already exist. 561 // 562 // A "=~" prefix is added to the stringified regexp in the statistics 563 // returned by GetCallCountInfo(). 564 // 565 // See RegisterResponder function and the "=~" prefix in its url 566 // parameter to avoid compiling the regexp by yourself. 567 // 568 // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, 569 // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible 570 // mistake. This panic can be disabled by setting m.DontCheckMethod to 571 // true prior to this call. 572 func (m *MockTransport) RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { 573 m.checkMethod(method) 574 575 m.registerRegexpResponder(regexpResponder{ 576 origRx: regexpPrefix + urlRegexp.String(), 577 method: method, 578 rx: urlRegexp, 579 responder: responder, 580 }) 581 } 582 583 // RegisterResponderWithQuery is same as RegisterResponder, but it 584 // doesn't depend on query items order. 585 // 586 // If query is non-nil, its type can be: 587 // url.Values 588 // map[string]string 589 // string, a query string like "a=12&a=13&b=z&c" (see net/url.ParseQuery function) 590 // 591 // If the query type is not recognized or the string cannot be parsed 592 // using net/url.ParseQuery, a panic() occurs. 593 // 594 // Unlike RegisterResponder, path cannot be prefixed by "=~" to say it 595 // is a regexp. If it is, a panic occurs. 596 // 597 // Registering an already existing responder resets the corresponding 598 // statistics as returned by GetCallCountInfo(). 599 // 600 // Registering a nil Responder removes the existing one and the 601 // corresponding statistics as returned by GetCallCountInfo(). It does 602 // nothing if it does not already exist. 603 // 604 // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, 605 // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible 606 // mistake. This panic can be disabled by setting m.DontCheckMethod to 607 // true prior to this call. 608 func (m *MockTransport) RegisterResponderWithQuery(method, path string, query any, responder Responder) { 609 if isRegexpURL(path) { 610 panic(`path begins with "=~", RegisterResponder should be used instead of RegisterResponderWithQuery`) 611 } 612 613 var mapQuery url.Values 614 switch q := query.(type) { 615 case url.Values: 616 mapQuery = q 617 618 case map[string]string: 619 mapQuery = make(url.Values, len(q)) 620 for key, e := range q { 621 mapQuery[key] = []string{e} 622 } 623 624 case string: 625 var err error 626 mapQuery, err = url.ParseQuery(q) 627 if err != nil { 628 panic("RegisterResponderWithQuery bad query string: " + err.Error()) 629 } 630 631 default: 632 if query != nil { 633 panic(fmt.Sprintf("RegisterResponderWithQuery bad query type %T. Only url.Values, map[string]string and string are allowed", query)) 634 } 635 } 636 637 if queryString := sortedQuery(mapQuery); queryString != "" { 638 path += "?" + queryString 639 } 640 m.RegisterResponder(method, path, responder) 641 } 642 643 func sortedQuery(m url.Values) string { 644 if len(m) == 0 { 645 return "" 646 } 647 648 keys := make([]string, 0, len(m)) 649 for k := range m { 650 keys = append(keys, k) 651 } 652 sort.Strings(keys) 653 654 var b bytes.Buffer 655 var values []string // nolint: prealloc 656 657 for _, k := range keys { 658 // Do not alter the passed url.Values 659 values = append(values, m[k]...) 660 sort.Strings(values) 661 662 k = url.QueryEscape(k) 663 664 for _, v := range values { 665 if b.Len() > 0 { 666 b.WriteByte('&') 667 } 668 fmt.Fprintf(&b, "%v=%v", k, url.QueryEscape(v)) 669 } 670 671 values = values[:0] 672 } 673 674 return b.String() 675 } 676 677 // RegisterNoResponder is used to register a responder that is called 678 // if no other responder is found. The default is httpmock.ConnectionFailure 679 // that returns an error able to indicate a possible method mismatch. 680 // 681 // Use it in conjunction with NewNotFoundResponder to ensure that all 682 // routes have been mocked: 683 // 684 // import ( 685 // "testing" 686 // "github.com/jarcoal/httpmock" 687 // ) 688 // ... 689 // func TestMyApp(t *testing.T) { 690 // ... 691 // // Calls testing.Fatal with the name of Responder-less route and 692 // // the stack trace of the call. 693 // httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) 694 // 695 // Will abort the current test and print something like: 696 // transport_test.go:735: Called from net/http.Get() 697 // at /go/src/github.com/jarcoal/httpmock/transport_test.go:714 698 // github.com/jarcoal/httpmock.TestCheckStackTracer() 699 // at /go/src/testing/testing.go:865 700 // testing.tRunner() 701 // at /go/src/runtime/asm_amd64.s:1337 702 // 703 // If responder is passed as nil, the default behavior 704 // (httpmock.ConnectionFailure) is re-enabled. 705 func (m *MockTransport) RegisterNoResponder(responder Responder) { 706 m.mu.Lock() 707 m.noResponder = responder 708 m.mu.Unlock() 709 } 710 711 // Reset removes all registered responders (including the no 712 // responder) from the MockTransport. It zeroes call counters too. 713 func (m *MockTransport) Reset() { 714 m.mu.Lock() 715 m.responders = make(map[internal.RouteKey]Responder) 716 m.regexpResponders = nil 717 m.noResponder = nil 718 m.callCountInfo = make(map[internal.RouteKey]int) 719 m.totalCallCount = 0 720 m.mu.Unlock() 721 } 722 723 // ZeroCallCounters zeroes call counters without touching registered responders. 724 func (m *MockTransport) ZeroCallCounters() { 725 m.mu.Lock() 726 for k := range m.callCountInfo { 727 m.callCountInfo[k] = 0 728 } 729 m.totalCallCount = 0 730 m.mu.Unlock() 731 } 732 733 // GetCallCountInfo gets the info on all the calls httpmock has caught 734 // since it was activated or reset. The info is returned as a map of 735 // the calling keys with the number of calls made to them as their 736 // value. The key is the method, a space, and the url all concatenated 737 // together. 738 // 739 // As a special case, regexp responders generate 2 entries for each 740 // call. One for the call caught and the other for the rule that 741 // matched. For example: 742 // RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body")) 743 // http.Get("http://z.com") 744 // 745 // will generate the following result: 746 // map[string]int{ 747 // `GET http://z.com`: 1, 748 // `GET =~z\.com\z`: 1, 749 // } 750 func (m *MockTransport) GetCallCountInfo() map[string]int { 751 m.mu.RLock() 752 res := make(map[string]int, len(m.callCountInfo)) 753 for k, v := range m.callCountInfo { 754 res[k.String()] = v 755 } 756 m.mu.RUnlock() 757 return res 758 } 759 760 // GetTotalCallCount returns the totalCallCount. 761 func (m *MockTransport) GetTotalCallCount() int { 762 m.mu.RLock() 763 defer m.mu.RUnlock() 764 return m.totalCallCount 765 } 766 767 // DefaultTransport is the default mock transport used by Activate, 768 // Deactivate, Reset, DeactivateAndReset, RegisterResponder, and 769 // RegisterNoResponder. 770 var DefaultTransport = NewMockTransport() 771 772 // InitialTransport is a cache of the original transport used so we 773 // can put it back when Deactivate is called. 774 var InitialTransport = http.DefaultTransport 775 776 // oldClients is used to handle custom http clients (i.e clients other 777 // than http.DefaultClient). 778 var oldClients = map[*http.Client]http.RoundTripper{} 779 780 // oldClientsLock protects oldClients from concurrent writes. 781 var oldClientsLock sync.Mutex 782 783 // Activate starts the mock environment. This should be called before 784 // your tests run. Under the hood this replaces the Transport on the 785 // http.DefaultClient with httpmock.DefaultTransport. 786 // 787 // To enable mocks for a test, simply activate at the beginning of a test: 788 // func TestFetchArticles(t *testing.T) { 789 // httpmock.Activate() 790 // // all http requests using http.DefaultTransport will now be intercepted 791 // } 792 // 793 // If you want all of your tests in a package to be mocked, just call 794 // Activate from init(): 795 // func init() { 796 // httpmock.Activate() 797 // } 798 // 799 // or using a TestMain function: 800 // func TestMain(m *testing.M) { 801 // httpmock.Activate() 802 // os.Exit(m.Run()) 803 // } 804 func Activate() { 805 if Disabled() { 806 return 807 } 808 809 // make sure that if Activate is called multiple times it doesn't 810 // overwrite the InitialTransport with a mock transport. 811 if http.DefaultTransport != DefaultTransport { 812 InitialTransport = http.DefaultTransport 813 } 814 815 http.DefaultTransport = DefaultTransport 816 } 817 818 // ActivateNonDefault starts the mock environment with a non-default 819 // http.Client. This emulates the Activate function, but allows for 820 // custom clients that do not use http.DefaultTransport 821 // 822 // To enable mocks for a test using a custom client, activate at the 823 // beginning of a test: 824 // client := &http.Client{Transport: &http.Transport{TLSHandshakeTimeout: 60 * time.Second}} 825 // httpmock.ActivateNonDefault(client) 826 func ActivateNonDefault(client *http.Client) { 827 if Disabled() { 828 return 829 } 830 831 // save the custom client & it's RoundTripper 832 oldClientsLock.Lock() 833 defer oldClientsLock.Unlock() 834 if _, ok := oldClients[client]; !ok { 835 oldClients[client] = client.Transport 836 } 837 client.Transport = DefaultTransport 838 } 839 840 // GetCallCountInfo gets the info on all the calls httpmock has caught 841 // since it was activated or reset. The info is returned as a map of 842 // the calling keys with the number of calls made to them as their 843 // value. The key is the method, a space, and the url all concatenated 844 // together. 845 // 846 // As a special case, regexp responders generate 2 entries for each 847 // call. One for the call caught and the other for the rule that 848 // matched. For example: 849 // RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body")) 850 // http.Get("http://z.com") 851 // 852 // will generate the following result: 853 // map[string]int{ 854 // `GET http://z.com`: 1, 855 // `GET =~z\.com\z`: 1, 856 // } 857 func GetCallCountInfo() map[string]int { 858 return DefaultTransport.GetCallCountInfo() 859 } 860 861 // GetTotalCallCount gets the total number of calls httpmock has taken 862 // since it was activated or reset. 863 func GetTotalCallCount() int { 864 return DefaultTransport.GetTotalCallCount() 865 } 866 867 // Deactivate shuts down the mock environment. Any HTTP calls made 868 // after this will use a live transport. 869 // 870 // Usually you'll call it in a defer right after activating the mock 871 // environment: 872 // func TestFetchArticles(t *testing.T) { 873 // httpmock.Activate() 874 // defer httpmock.Deactivate() 875 // 876 // // when this test ends, the mock environment will close 877 // } 878 // 879 // Since go 1.14 you can also use (*testing.T).Cleanup() method as in: 880 // func TestFetchArticles(t *testing.T) { 881 // httpmock.Activate() 882 // t.Cleanup(httpmock.Deactivate) 883 // 884 // // when this test ends, the mock environment will close 885 // } 886 // 887 // useful in test helpers to save your callers from calling defer themselves. 888 func Deactivate() { 889 if Disabled() { 890 return 891 } 892 http.DefaultTransport = InitialTransport 893 894 // reset the custom clients to use their original RoundTripper 895 oldClientsLock.Lock() 896 defer oldClientsLock.Unlock() 897 for oldClient, oldTransport := range oldClients { 898 oldClient.Transport = oldTransport 899 delete(oldClients, oldClient) 900 } 901 } 902 903 // Reset removes any registered mocks and returns the mock 904 // environment to its initial state. It zeroes call counters too. 905 func Reset() { 906 DefaultTransport.Reset() 907 } 908 909 // ZeroCallCounters zeroes call counters without touching registered responders. 910 func ZeroCallCounters() { 911 DefaultTransport.ZeroCallCounters() 912 } 913 914 // DeactivateAndReset is just a convenience method for calling 915 // Deactivate() and then Reset(). 916 // 917 // Happy deferring! 918 func DeactivateAndReset() { 919 Deactivate() 920 Reset() 921 } 922 923 // RegisterResponder adds a new responder, associated with a given 924 // HTTP method and URL (or path). 925 // 926 // When a request comes in that matches, the responder is called and 927 // the response returned to the client. 928 // 929 // If url contains query parameters, their order matters as well as 930 // their content. All following URLs are here considered as different: 931 // http://z.tld?a=1&b=1 932 // http://z.tld?b=1&a=1 933 // http://z.tld?a&b 934 // http://z.tld?a=&b= 935 // 936 // If url begins with "=~", the following chars are considered as a 937 // regular expression. If this regexp can not be compiled, it panics. 938 // Note that the "=~" prefix remains in statistics returned by 939 // GetCallCountInfo(). As 2 regexps can match the same URL, the regexp 940 // responders are tested in the order they are registered. Registering 941 // an already existing regexp responder (same method & same regexp 942 // string) replaces its responder, but does not change its position. 943 // 944 // Registering an already existing responder resets the corresponding 945 // statistics as returned by GetCallCountInfo(). 946 // 947 // Registering a nil Responder removes the existing one and the 948 // corresponding statistics as returned by GetCallCountInfo(). It does 949 // nothing if it does not already exist. 950 // 951 // See RegisterRegexpResponder() to directly pass a *regexp.Regexp. 952 // 953 // Example: 954 // func TestFetchArticles(t *testing.T) { 955 // httpmock.Activate() 956 // defer httpmock.DeactivateAndReset() 957 // 958 // httpmock.RegisterResponder("GET", "http://example.com/", 959 // httpmock.NewStringResponder(200, "hello world")) 960 // 961 // httpmock.RegisterResponder("GET", "/path/only", 962 // httpmock.NewStringResponder("any host hello world", 200)) 963 // 964 // httpmock.RegisterResponder("GET", `=~^/item/id/\d+\z`, 965 // httpmock.NewStringResponder("any item get", 200)) 966 // 967 // // requests to http://example.com/ now return "hello world" and 968 // // requests to any host with path /path/only return "any host hello world" 969 // // requests to any host with path matching ^/item/id/\d+\z regular expression return "any item get" 970 // } 971 // 972 // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, 973 // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible 974 // mistake. This panic can be disabled by setting 975 // DefaultTransport.DontCheckMethod to true prior to this call. 976 func RegisterResponder(method, url string, responder Responder) { 977 DefaultTransport.RegisterResponder(method, url, responder) 978 } 979 980 // RegisterRegexpResponder adds a new responder, associated with a given 981 // HTTP method and URL (or path) regular expression. 982 // 983 // When a request comes in that matches, the responder is called and 984 // the response returned to the client. 985 // 986 // As 2 regexps can match the same URL, the regexp responders are 987 // tested in the order they are registered. Registering an already 988 // existing regexp responder (same method & same regexp string) 989 // replaces its responder, but does not change its position, and 990 // resets the corresponding statistics as returned by GetCallCountInfo(). 991 // 992 // Registering a nil Responder removes the existing one and the 993 // corresponding statistics as returned by GetCallCountInfo(). It does 994 // nothing if it does not already exist. 995 // 996 // A "=~" prefix is added to the stringified regexp in the statistics 997 // returned by GetCallCountInfo(). 998 // 999 // See RegisterResponder function and the "=~" prefix in its url 1000 // parameter to avoid compiling the regexp by yourself. 1001 // 1002 // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, 1003 // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible 1004 // mistake. This panic can be disabled by setting 1005 // DefaultTransport.DontCheckMethod to true prior to this call. 1006 func RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { 1007 DefaultTransport.RegisterRegexpResponder(method, urlRegexp, responder) 1008 } 1009 1010 // RegisterResponderWithQuery it is same as RegisterResponder, but 1011 // doesn't depends on query items order. 1012 // 1013 // query type can be: 1014 // url.Values 1015 // map[string]string 1016 // string, a query string like "a=12&a=13&b=z&c" (see net/url.ParseQuery function) 1017 // 1018 // If the query type is not recognized or the string cannot be parsed 1019 // using net/url.ParseQuery, a panic() occurs. 1020 // 1021 // Unlike RegisterResponder, path cannot be prefixed by "=~" to say it 1022 // is a regexp. If it is, a panic occurs. 1023 // 1024 // Registering an already existing responder resets the corresponding 1025 // statistics as returned by GetCallCountInfo(). 1026 // 1027 // Registering a nil Responder removes the existing one and the 1028 // corresponding statistics as returned by GetCallCountInfo(). It does 1029 // nothing if it does not already exist. 1030 // 1031 // Example using a net/url.Values: 1032 // func TestFetchArticles(t *testing.T) { 1033 // httpmock.Activate() 1034 // defer httpmock.DeactivateAndReset() 1035 // 1036 // expectedQuery := net.Values{ 1037 // "a": []string{"3", "1", "8"}, 1038 // "b": []string{"4", "2"}, 1039 // } 1040 // httpmock.RegisterResponderWithQueryValues( 1041 // "GET", "http://example.com/", expectedQuery, 1042 // httpmock.NewStringResponder("hello world", 200)) 1043 // 1044 // // requests to http://example.com?a=1&a=3&a=8&b=2&b=4 1045 // // and to http://example.com?b=4&a=2&b=2&a=8&a=1 1046 // // now return 'hello world' 1047 // } 1048 // 1049 // or using a map[string]string: 1050 // func TestFetchArticles(t *testing.T) { 1051 // httpmock.Activate() 1052 // defer httpmock.DeactivateAndReset() 1053 // 1054 // expectedQuery := map[string]string{ 1055 // "a": "1", 1056 // "b": "2" 1057 // } 1058 // httpmock.RegisterResponderWithQuery( 1059 // "GET", "http://example.com/", expectedQuery, 1060 // httpmock.NewStringResponder("hello world", 200)) 1061 // 1062 // // requests to http://example.com?a=1&b=2 and http://example.com?b=2&a=1 now return 'hello world' 1063 // } 1064 // 1065 // or using a query string: 1066 // func TestFetchArticles(t *testing.T) { 1067 // httpmock.Activate() 1068 // defer httpmock.DeactivateAndReset() 1069 // 1070 // expectedQuery := "a=3&b=4&b=2&a=1&a=8" 1071 // httpmock.RegisterResponderWithQueryValues( 1072 // "GET", "http://example.com/", expectedQuery, 1073 // httpmock.NewStringResponder("hello world", 200)) 1074 // 1075 // // requests to http://example.com?a=1&a=3&a=8&b=2&b=4 1076 // // and to http://example.com?b=4&a=2&b=2&a=8&a=1 1077 // // now return 'hello world' 1078 // } 1079 // 1080 // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, 1081 // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible 1082 // mistake. This panic can be disabled by setting 1083 // DefaultTransport.DontCheckMethod to true prior to this call. 1084 func RegisterResponderWithQuery(method, path string, query any, responder Responder) { 1085 DefaultTransport.RegisterResponderWithQuery(method, path, query, responder) 1086 } 1087 1088 // RegisterNoResponder adds a mock that is called whenever a request 1089 // for an unregistered URL is received. The default behavior is to 1090 // return a connection error. 1091 // 1092 // In some cases you may not want all URLs to be mocked, in which case 1093 // you can do this: 1094 // func TestFetchArticles(t *testing.T) { 1095 // httpmock.Activate() 1096 // defer httpmock.DeactivateAndReset() 1097 // httpmock.RegisterNoResponder(httpmock.InitialTransport.RoundTrip) 1098 // 1099 // // any requests that don't have a registered URL will be fetched normally 1100 // } 1101 func RegisterNoResponder(responder Responder) { 1102 DefaultTransport.RegisterNoResponder(responder) 1103 } 1104 1105 // ErrSubmatchNotFound is the error returned by GetSubmatch* functions 1106 // when the given submatch index cannot be found. 1107 var ErrSubmatchNotFound = errors.New("submatch not found") 1108 1109 // GetSubmatch has to be used in Responders installed by 1110 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1111 // allows to retrieve the n-th submatch of the matching regexp, as a 1112 // string. Example: 1113 // RegisterResponder("GET", `=~^/item/name/([^/]+)\z`, 1114 // func(req *http.Request) (*http.Response, error) { 1115 // name, err := GetSubmatch(req, 1) // 1=first regexp submatch 1116 // if err != nil { 1117 // return nil, err 1118 // } 1119 // return NewJsonResponse(200, map[string]any{ 1120 // "id": 123, 1121 // "name": name, 1122 // }) 1123 // }) 1124 // 1125 // It panics if n < 1. See MustGetSubmatch to avoid testing the 1126 // returned error. 1127 func GetSubmatch(req *http.Request, n int) (string, error) { 1128 if n <= 0 { 1129 panic(fmt.Sprintf("getting submatches starts at 1, not %d", n)) 1130 } 1131 n-- 1132 1133 submatches := internal.GetSubmatches(req) 1134 if n >= len(submatches) { 1135 return "", ErrSubmatchNotFound 1136 } 1137 return submatches[n], nil 1138 } 1139 1140 // GetSubmatchAsInt has to be used in Responders installed by 1141 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1142 // allows to retrieve the n-th submatch of the matching regexp, as an 1143 // int64. Example: 1144 // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, 1145 // func(req *http.Request) (*http.Response, error) { 1146 // id, err := GetSubmatchAsInt(req, 1) // 1=first regexp submatch 1147 // if err != nil { 1148 // return nil, err 1149 // } 1150 // return NewJsonResponse(200, map[string]any{ 1151 // "id": id, 1152 // "name": "The beautiful name", 1153 // }) 1154 // }) 1155 // 1156 // It panics if n < 1. See MustGetSubmatchAsInt to avoid testing the 1157 // returned error. 1158 func GetSubmatchAsInt(req *http.Request, n int) (int64, error) { 1159 sm, err := GetSubmatch(req, n) 1160 if err != nil { 1161 return 0, err 1162 } 1163 return strconv.ParseInt(sm, 10, 64) 1164 } 1165 1166 // GetSubmatchAsUint has to be used in Responders installed by 1167 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1168 // allows to retrieve the n-th submatch of the matching regexp, as a 1169 // uint64. Example: 1170 // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, 1171 // func(req *http.Request) (*http.Response, error) { 1172 // id, err := GetSubmatchAsUint(req, 1) // 1=first regexp submatch 1173 // if err != nil { 1174 // return nil, err 1175 // } 1176 // return NewJsonResponse(200, map[string]any{ 1177 // "id": id, 1178 // "name": "The beautiful name", 1179 // }) 1180 // }) 1181 // 1182 // It panics if n < 1. See MustGetSubmatchAsUint to avoid testing the 1183 // returned error. 1184 func GetSubmatchAsUint(req *http.Request, n int) (uint64, error) { 1185 sm, err := GetSubmatch(req, n) 1186 if err != nil { 1187 return 0, err 1188 } 1189 return strconv.ParseUint(sm, 10, 64) 1190 } 1191 1192 // GetSubmatchAsFloat has to be used in Responders installed by 1193 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1194 // allows to retrieve the n-th submatch of the matching regexp, as a 1195 // float64. Example: 1196 // RegisterResponder("PATCH", `=~^/item/id/\d+\?height=(\d+(?:\.\d*)?)\z`, 1197 // func(req *http.Request) (*http.Response, error) { 1198 // height, err := GetSubmatchAsFloat(req, 1) // 1=first regexp submatch 1199 // if err != nil { 1200 // return nil, err 1201 // } 1202 // return NewJsonResponse(200, map[string]any{ 1203 // "id": id, 1204 // "name": "The beautiful name", 1205 // "height": height, 1206 // }) 1207 // }) 1208 // 1209 // It panics if n < 1. See MustGetSubmatchAsFloat to avoid testing the 1210 // returned error. 1211 func GetSubmatchAsFloat(req *http.Request, n int) (float64, error) { 1212 sm, err := GetSubmatch(req, n) 1213 if err != nil { 1214 return 0, err 1215 } 1216 return strconv.ParseFloat(sm, 64) 1217 } 1218 1219 // MustGetSubmatch works as GetSubmatch except that it panics in case 1220 // of error (submatch not found). It has to be used in Responders 1221 // installed by RegisterRegexpResponder or RegisterResponder + "=~" 1222 // URL prefix. It allows to retrieve the n-th submatch of the matching 1223 // regexp, as a string. Example: 1224 // RegisterResponder("GET", `=~^/item/name/([^/]+)\z`, 1225 // func(req *http.Request) (*http.Response, error) { 1226 // name := MustGetSubmatch(req, 1) // 1=first regexp submatch 1227 // return NewJsonResponse(200, map[string]any{ 1228 // "id": 123, 1229 // "name": name, 1230 // }) 1231 // }) 1232 // 1233 // It panics if n < 1. 1234 func MustGetSubmatch(req *http.Request, n int) string { 1235 s, err := GetSubmatch(req, n) 1236 if err != nil { 1237 panic("GetSubmatch failed: " + err.Error()) 1238 } 1239 return s 1240 } 1241 1242 // MustGetSubmatchAsInt works as GetSubmatchAsInt except that it 1243 // panics in case of error (submatch not found or invalid int64 1244 // format). It has to be used in Responders installed by 1245 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1246 // allows to retrieve the n-th submatch of the matching regexp, as an 1247 // int64. Example: 1248 // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, 1249 // func(req *http.Request) (*http.Response, error) { 1250 // id := MustGetSubmatchAsInt(req, 1) // 1=first regexp submatch 1251 // return NewJsonResponse(200, map[string]any{ 1252 // "id": id, 1253 // "name": "The beautiful name", 1254 // }) 1255 // }) 1256 // 1257 // It panics if n < 1. 1258 func MustGetSubmatchAsInt(req *http.Request, n int) int64 { 1259 i, err := GetSubmatchAsInt(req, n) 1260 if err != nil { 1261 panic("GetSubmatchAsInt failed: " + err.Error()) 1262 } 1263 return i 1264 } 1265 1266 // MustGetSubmatchAsUint works as GetSubmatchAsUint except that it 1267 // panics in case of error (submatch not found or invalid uint64 1268 // format). It has to be used in Responders installed by 1269 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1270 // allows to retrieve the n-th submatch of the matching regexp, as a 1271 // uint64. Example: 1272 // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, 1273 // func(req *http.Request) (*http.Response, error) { 1274 // id, err := MustGetSubmatchAsUint(req, 1) // 1=first regexp submatch 1275 // return NewJsonResponse(200, map[string]any{ 1276 // "id": id, 1277 // "name": "The beautiful name", 1278 // }) 1279 // }) 1280 // 1281 // It panics if n < 1. 1282 func MustGetSubmatchAsUint(req *http.Request, n int) uint64 { 1283 u, err := GetSubmatchAsUint(req, n) 1284 if err != nil { 1285 panic("GetSubmatchAsUint failed: " + err.Error()) 1286 } 1287 return u 1288 } 1289 1290 // MustGetSubmatchAsFloat works as GetSubmatchAsFloat except that it 1291 // panics in case of error (submatch not found or invalid float64 1292 // format). It has to be used in Responders installed by 1293 // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It 1294 // allows to retrieve the n-th submatch of the matching regexp, as a 1295 // float64. Example: 1296 // RegisterResponder("PATCH", `=~^/item/id/\d+\?height=(\d+(?:\.\d*)?)\z`, 1297 // func(req *http.Request) (*http.Response, error) { 1298 // height := MustGetSubmatchAsFloat(req, 1) // 1=first regexp submatch 1299 // return NewJsonResponse(200, map[string]any{ 1300 // "id": id, 1301 // "name": "The beautiful name", 1302 // "height": height, 1303 // }) 1304 // }) 1305 // 1306 // It panics if n < 1. 1307 func MustGetSubmatchAsFloat(req *http.Request, n int) float64 { 1308 f, err := GetSubmatchAsFloat(req, n) 1309 if err != nil { 1310 panic("GetSubmatchAsFloat failed: " + err.Error()) 1311 } 1312 return f 1313 } 1314