pax_global_header00006660000000000000000000000064141605617220014516gustar00rootroot0000000000000052 comment=a62b8743d70c22c0912a30ab26c46fd5de775ac5 httpmock-1.1.0/000077500000000000000000000000001416056172200133465ustar00rootroot00000000000000httpmock-1.1.0/.github/000077500000000000000000000000001416056172200147065ustar00rootroot00000000000000httpmock-1.1.0/.github/workflows/000077500000000000000000000000001416056172200167435ustar00rootroot00000000000000httpmock-1.1.0/.github/workflows/ci.yml000066400000000000000000000051021416056172200200570ustar00rootroot00000000000000name: Build on: push: branches: [ v1 ] pull_request: branches: [ v1 ] jobs: test: strategy: matrix: go-version: [1.7.x, 1.8.x, 1.9.x, 1.10.x, 1.11.x, 1.12.x, 1.13.x, 1.14.x, 1.15.x, 1.16.x, tip] full-tests: [false] include: - go-version: 1.17.x full-tests: true runs-on: ubuntu-latest steps: - name: Setup go run: | curl -sL https://raw.githubusercontent.com/maxatome/install-go/v3.0/install-go.pl | perl - ${{ matrix.go-version }} $HOME/go - name: Checkout code uses: actions/checkout@v2 - name: Linting if: matrix.full-tests run: | curl -sL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $HOME/go/bin v1.43.0 $HOME/go/bin/golangci-lint run --max-issues-per-linter 0 \ --max-same-issues 0 \ -E bidichk \ -E exportloopref \ -E gocritic \ -E godot \ -E goimports \ -E maligned \ -E misspell \ -E prealloc \ -E revive \ -E unconvert \ -E whitespace \ ./... - name: Testing continue-on-error: ${{ matrix.go-version == 'tip' }} run: | go version if [ ${{ matrix.full-tests }} = true ]; then GO_TEST_OPTS="-covermode=atomic -coverprofile=coverage.out" fi case ${{ matrix.go-version }} in 1.[789].x | 1.10.x) # Before go 1.11, go modules are not available mkdir -p ../src/github.com/$GITHUB_REPOSITORY_OWNER ln -s $(pwd) ../src/github.com/$GITHUB_REPOSITORY export GOPATH=$(dirname $(pwd)) cd $GOPATH/src/github.com/$GITHUB_REPOSITORY ;; esac export GORACE="halt_on_error=1" go test -race $GO_TEST_OPTS ./... - name: Reporting if: matrix.full-tests env: COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | go get github.com/mattn/goveralls goveralls -coverprofile=coverage.out -service=github httpmock-1.1.0/.gitignore000066400000000000000000000003741416056172200153420ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe httpmock-1.1.0/LICENSE000066400000000000000000000020661416056172200143570ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2014 Jared Morse Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. httpmock-1.1.0/README.md000066400000000000000000000162431416056172200146330ustar00rootroot00000000000000# httpmock [![Build Status](https://github.com/jarcoal/httpmock/workflows/Build/badge.svg?branch=v1)](https://github.com/jarcoal/httpmock/actions?query=workflow%3ABuild) [![Coverage Status](https://coveralls.io/repos/github/jarcoal/httpmock/badge.svg?branch=v1)](https://coveralls.io/github/jarcoal/httpmock?branch=v1) [![GoDoc](https://godoc.org/github.com/jarcoal/httpmock?status.svg)](https://godoc.org/github.com/jarcoal/httpmock) [![Version](https://img.shields.io/github/tag/jarcoal/httpmock.svg)](https://github.com/jarcoal/httpmock/releases) [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go/#testing) Easy mocking of http responses from external resources. ## Install Currently supports Go 1.7 - 1.17. `v1` branch has to be used instead of `master`. ### Using go modules (aka. `go mod`) In your go files, simply use: ```go import "github.com/jarcoal/httpmock" ``` Then next `go mod tidy` or `go test` invocation will automatically populate your `go.mod` with the last httpmock release, now [![Version](https://img.shields.io/github/tag/jarcoal/httpmock.svg)](https://github.com/jarcoal/httpmock/releases). Note you can use `go mod vendor` to vendor your dependencies. ### Using `$GOPATH` `v1` branch is configured as the default branch in github, so: ``` go get github.com/jarcoal/httpmock ``` automatically downloads the `v1` branch in `$GOPATH/src`. Then in your go files use: ```go import "github.com/jarcoal/httpmock" ``` ### Vendoring, using [`govendor`](https://github.com/kardianos/govendor) for example When vendoring is used, `v1` branch has to be specified. Two choices here: - preferred way: ``` govendor fetch github.com/jarcoal/httpmock@v1 ``` then in go files: ```go import "github.com/jarcoal/httpmock" ``` - old way (before `v1` was set as default branch), use gopkg to read from `v1` branch: ``` govendor fetch gopkg.in/jarcoal/httpmock.v1 ``` then in go files: ```go import "gopkg.in/jarcoal/httpmock.v1" ``` ## Usage ### Simple Example: ```go func TestFetchArticles(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() // Exact URL match httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles", httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`)) // Regexp match (could use httpmock.RegisterRegexpResponder instead) httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/\d+\z`, httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`)) // do stuff that makes a request to articles ... // get count info httpmock.GetTotalCallCount() // get the amount of calls for the registered responder info := httpmock.GetCallCountInfo() info["GET https://api.mybiz.com/articles"] // number of GET calls made to https://api.mybiz.com/articles info["GET https://api.mybiz.com/articles/id/12"] // number of GET calls made to https://api.mybiz.com/articles/id/12 info[`GET =~^https://api\.mybiz\.com/articles/id/\d+\z`] // number of GET calls made to https://api.mybiz.com/articles/id/ } ``` ### Advanced Example: ```go func TestFetchArticles(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() // our database of articles articles := make([]map[string]interface{}, 0) // mock to list out the articles httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles", func(req *http.Request) (*http.Response, error) { resp, err := httpmock.NewJsonResponse(200, articles) if err != nil { return httpmock.NewStringResponse(500, ""), nil } return resp, nil }, ) // return an article related to the request with the help of regexp submatch (\d+) httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/(\d+)\z`, func(req *http.Request) (*http.Response, error) { // Get ID from request id := httpmock.MustGetSubmatchAsUint(req, 1) // 1=first regexp submatch return httpmock.NewJsonResponse(200, map[string]interface{}{ "id": id, "name": "My Great Article", }) }, ) // mock to add a new article httpmock.RegisterResponder("POST", "https://api.mybiz.com/articles", func(req *http.Request) (*http.Response, error) { article := make(map[string]interface{}) if err := json.NewDecoder(req.Body).Decode(&article); err != nil { return httpmock.NewStringResponse(400, ""), nil } articles = append(articles, article) resp, err := httpmock.NewJsonResponse(200, article) if err != nil { return httpmock.NewStringResponse(500, ""), nil } return resp, nil }, ) // do stuff that adds and checks articles } ``` ### Algorithm When `GET http://example.tld/some/path?b=12&a=foo&a=bar` request is caught, all standard responders are checked against the following URL or paths, the first match stops the search: 1. `http://example.tld/some/path?b=12&a=foo&a=bar` (original URL) 1. `http://example.tld/some/path?a=bar&a=foo&b=12` (sorted query params) 1. `http://example.tld/some/path` (without query params) 1. `/some/path?b=12&a=foo&a=bar` (original URL without scheme and host) 1. `/some/path?a=bar&a=foo&b=12` (same, but sorted query params) 1. `/some/path` (path only) If no standard responder matched, the regexp responders are checked, in the same order, the first match stops the search. ### [Ginkgo](https://onsi.github.io/ginkgo/) Example: ```go // article_suite_test.go import ( // ... "github.com/jarcoal/httpmock" ) // ... var _ = BeforeSuite(func() { // block all HTTP requests httpmock.Activate() }) var _ = BeforeEach(func() { // remove any mocks httpmock.Reset() }) var _ = AfterSuite(func() { httpmock.DeactivateAndReset() }) // article_test.go import ( // ... "github.com/jarcoal/httpmock" ) var _ = Describe("Articles", func() { It("returns a list of articles", func() { httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles.json", httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`)) // do stuff that makes a request to articles.json }) }) ``` ### [Ginkgo](https://onsi.github.io/ginkgo/) + [Resty](https://github.com/go-resty/resty) Example: ```go // article_suite_test.go import ( // ... "github.com/jarcoal/httpmock" "github.com/go-resty/resty" ) // ... var _ = BeforeSuite(func() { // block all HTTP requests httpmock.ActivateNonDefault(resty.DefaultClient.GetClient()) }) var _ = BeforeEach(func() { // remove any mocks httpmock.Reset() }) var _ = AfterSuite(func() { httpmock.DeactivateAndReset() }) // article_test.go import ( // ... "github.com/jarcoal/httpmock" "github.com/go-resty/resty" ) var _ = Describe("Articles", func() { It("returns a list of articles", func() { fixture := `{"status":{"message": "Your message", "code": 200}}` responder := httpmock.NewStringResponder(200, fixture) fakeUrl := "https://api.mybiz.com/articles.json" httpmock.RegisterResponder("GET", fakeUrl, responder) // fetch the article into struct articleObject := &models.Article{} _, err := resty.R().SetResult(articleObject).Get(fakeUrl) // do stuff with the article object ... }) }) ``` httpmock-1.1.0/doc.go000066400000000000000000000055631416056172200144530ustar00rootroot00000000000000/* Package httpmock provides tools for mocking HTTP responses. Simple Example: func TestFetchArticles(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() // Exact URL match httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles", httpmock.NewStringResponder(200, `[{"id": 1, "name": "My Great Article"}]`)) // Regexp match (could use httpmock.RegisterRegexpResponder instead) httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/\d+\z`, httpmock.NewStringResponder(200, `{"id": 1, "name": "My Great Article"}`)) // do stuff that makes a request to articles // get count info httpmock.GetTotalCallCount() // get the amount of calls for the registered responder info := httpmock.GetCallCountInfo() info["GET https://api.mybiz.com/articles"] // number of GET calls made to https://api.mybiz.com/articles info["GET https://api.mybiz.com/articles/id/12"] // number of GET calls made to https://api.mybiz.com/articles/id/12 info[`GET =~^https://api\.mybiz\.com/articles/id/\d+\z`] // number of GET calls made to https://api.mybiz.com/articles/id/ } Advanced Example: func TestFetchArticles(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() // our database of articles articles := make([]map[string]interface{}, 0) // mock to list out the articles httpmock.RegisterResponder("GET", "https://api.mybiz.com/articles", func(req *http.Request) (*http.Response, error) { resp, err := httpmock.NewJsonResponse(200, articles) if err != nil { return httpmock.NewStringResponse(500, ""), nil } return resp, nil }, ) // return an article related to the request with the help of regexp submatch (\d+) httpmock.RegisterResponder("GET", `=~^https://api\.mybiz\.com/articles/id/(\d+)\z`, func(req *http.Request) (*http.Response, error) { // Get ID from request id := httpmock.MustGetSubmatchAsUint(req, 1) // 1=first regexp submatch return httpmock.NewJsonResponse(200, map[string]interface{}{ "id": id, "name": "My Great Article", }) }, ) // mock to add a new article httpmock.RegisterResponder("POST", "https://api.mybiz.com/articles", func(req *http.Request) (*http.Response, error) { article := make(map[string]interface{}) if err := json.NewDecoder(req.Body).Decode(&article); err != nil { return httpmock.NewStringResponse(400, ""), nil } articles = append(articles, article) resp, err := httpmock.NewJsonResponse(200, article) if err != nil { return httpmock.NewStringResponse(500, ""), nil } return resp, nil }, ) // do stuff that adds and checks articles } */ package httpmock httpmock-1.1.0/env.go000066400000000000000000000003571416056172200144720ustar00rootroot00000000000000package httpmock import ( "os" ) var envVarName = "GONOMOCKS" // Disabled allows to test whether httpmock is enabled or not. It // depends on GONOMOCKS environment variable. func Disabled() bool { return os.Getenv(envVarName) != "" } httpmock-1.1.0/env_test.go000066400000000000000000000031541416056172200155270ustar00rootroot00000000000000package httpmock import ( "net/http" "os" "testing" ) func TestEnv(t *testing.T) { DeactivateAndReset() orig := os.Getenv(envVarName) // put it in an enabled state if err := os.Setenv(envVarName, ""); err != nil { t.Fatal(err) } else if Disabled() { t.Fatal("expected not to be disabled") } client1 := &http.Client{Transport: &http.Transport{}} client2 := &http.Client{Transport: &http.Transport{}} // make sure an activation works Activate() ActivateNonDefault(client1) ActivateNonDefault(client2) if http.DefaultTransport != DefaultTransport { t.Fatal("expected http.DefaultTransport to be our DefaultTransport") } if client1.Transport != DefaultTransport { t.Fatal("expected client1.Transport to be our DefaultTransport") } if client2.Transport != DefaultTransport { t.Fatal("expected client2.Transport to be our DefaultTransport") } Deactivate() if err := os.Setenv(envVarName, "1"); err != nil { t.Fatal(err) } else if !Disabled() { t.Fatal("expected to be disabled") } // make sure activation doesn't work Activate() ActivateNonDefault(client1) ActivateNonDefault(client2) if http.DefaultTransport == DefaultTransport { t.Fatal("expected http.DefaultTransport to not be our DefaultTransport") } if client1.Transport == DefaultTransport { t.Fatal("expected client1.Transport to not be our DefaultTransport") } if client2.Transport == DefaultTransport { t.Fatal("expected client2.Transport to not be our DefaultTransport") } Deactivate() if err := os.Setenv(envVarName, orig); err != nil { t.Fatalf("could not reset %s to it's original value '%s'", envVarName, orig) } } httpmock-1.1.0/file.go000066400000000000000000000031221416056172200146120ustar00rootroot00000000000000package httpmock import ( "fmt" "io/ioutil" ) // File is a file name. The contents of this file is loaded on demand // by the following methods. // // Note that: // file := httpmock.File("file.txt") // fmt.Printf("file: %s\n", file) // // prints the content of file "file.txt" as String() method is used. // // To print the file name, and not its content, simply do: // file := httpmock.File("file.txt") // fmt.Printf("file: %s\n", string(file)) type File string // MarshalJSON implements json.Marshaler. // // Useful to be used in conjunction with NewJsonResponse() or // NewJsonResponder() as in: // httpmock.NewJsonResponder(200, httpmock.File("body.json")) func (f File) MarshalJSON() ([]byte, error) { return f.bytes() } func (f File) bytes() ([]byte, error) { return ioutil.ReadFile(string(f)) } // Bytes returns the content of file as a []byte. If an error occurs // during the opening or reading of the file, it panics. // // Useful to be used in conjunction with NewBytesResponse() or // NewBytesResponder() as in: // httpmock.NewBytesResponder(200, httpmock.File("body.raw").Bytes()) func (f File) Bytes() []byte { b, err := f.bytes() if err != nil { panic(fmt.Sprintf("Cannot read %s: %s", string(f), err)) } return b } // String returns the content of file as a string. If an error occurs // during the opening or reading of the file, it panics. // // Useful to be used in conjunction with NewStringResponse() or // NewStringResponder() as in: // httpmock.NewStringResponder(200, httpmock.File("body.txt").String()) func (f File) String() string { return string(f.Bytes()) } httpmock-1.1.0/file_test.go000066400000000000000000000050521416056172200156550ustar00rootroot00000000000000package httpmock_test import ( "bytes" "encoding/json" "path/filepath" "strings" "testing" "github.com/jarcoal/httpmock" ) var _ json.Marshaler = httpmock.File("test.json") func TestFile(t *testing.T) { dir, cleanup := tmpDir(t) defer cleanup() t.Run("Valid JSON file", func(t *testing.T) { okFile := filepath.Join(dir, "ok.json") writeFile(t, okFile, []byte(`{ "test": true }`)) encoded, err := json.Marshal(httpmock.File(okFile)) if err != nil { t.Errorf("json.Marshal(%s) failed: %s", okFile, err) return } got, expected := string(encoded), `{"test":true}` if got != expected { t.Errorf("json.Marshal(%s): got=<%s> expected=<%s>", okFile, got, expected) } }) t.Run("Nonexistent JSON file", func(t *testing.T) { nonexistentFile := filepath.Join(dir, "nonexistent.json") _, err := json.Marshal(httpmock.File(nonexistentFile)) if err == nil { t.Errorf("json.Marshal(%s) succeeded, but an error is expected!", nonexistentFile) } }) t.Run("Invalid JSON file", func(t *testing.T) { badFile := filepath.Join(dir, "bad.json") writeFile(t, badFile, []byte(`[123`)) _, err := json.Marshal(httpmock.File(badFile)) if err == nil { t.Errorf("json.Marshal(%s) succeeded, but an error is expected!", badFile) } }) t.Run("Bytes", func(t *testing.T) { file := filepath.Join(dir, "ok.raw") content := []byte(`abc123`) writeFile(t, file, content) if got := httpmock.File(file).Bytes(); !bytes.Equal(content, got) { t.Errorf("bytes differ:\n got: %v\n expected: %v", got, content) } }) t.Run("Bytes panic", func(t *testing.T) { nonexistentFile := filepath.Join(dir, "nonexistent.raw") panicked, mesg := catchPanic(func() { httpmock.File(nonexistentFile).Bytes() }) if !panicked { t.Error("No panic detected") return } if !strings.HasPrefix(mesg, "Cannot read "+nonexistentFile) { t.Errorf("Bad panic mesg: <%s>", mesg) } }) t.Run("String", func(t *testing.T) { file := filepath.Join(dir, "ok.txt") content := `abc123` writeFile(t, file, []byte(content)) if got := httpmock.File(file).String(); got != content { t.Errorf("strings differ:\n got: <%s>\n expected: <%s>", got, content) } }) t.Run("String panic", func(t *testing.T) { nonexistentFile := filepath.Join(dir, "nonexistent.txt") panicked, mesg := catchPanic(func() { httpmock.File(nonexistentFile).String() //nolint: govet }) if !panicked { t.Error("No panic detected") return } if !strings.HasPrefix(mesg, "Cannot read "+nonexistentFile) { t.Errorf("Bad panic mesg: <%s>", mesg) } }) } httpmock-1.1.0/go.mod000066400000000000000000000000531416056172200144520ustar00rootroot00000000000000module github.com/jarcoal/httpmock go 1.7 httpmock-1.1.0/internal/000077500000000000000000000000001416056172200151625ustar00rootroot00000000000000httpmock-1.1.0/internal/error.go000066400000000000000000000021631416056172200166440ustar00rootroot00000000000000package internal import ( "errors" "fmt" ) // NoResponderFound is returned when no responders are found for a // given HTTP method and URL. var NoResponderFound = errors.New("no responder found") // nolint: revive // errorNoResponderFoundMethodCase encapsulates a NoResponderFound // error probably due to the method not upper-cased. type ErrorNoResponderFoundWrongMethod struct { orig string // original wrong method, without any matching responder suggested string // suggested method with a matching responder } // NewErrorNoResponderFoundWrongMethod returns an ErrorNoResponderFoundWrongMethod. func NewErrorNoResponderFoundWrongMethod(orig, suggested string) error { return &ErrorNoResponderFoundWrongMethod{ orig: orig, suggested: suggested, } } // Unwrap implements the interface needed by errors.Unwrap. func (e *ErrorNoResponderFoundWrongMethod) Unwrap() error { return NoResponderFound } // Error implements error interface. func (e *ErrorNoResponderFoundWrongMethod) Error() string { return fmt.Sprintf("%s for method %s, but one matches method %s", NoResponderFound, e.orig, e.suggested, ) } httpmock-1.1.0/internal/error_test.go000066400000000000000000000010241416056172200176760ustar00rootroot00000000000000package internal_test import ( "testing" "github.com/jarcoal/httpmock/internal" ) func TestErrorNoResponderFoundMethodCase(t *testing.T) { e := internal.NewErrorNoResponderFoundWrongMethod("pipo", "BINGO") if e.Error() != "no responder found for method pipo, but one matches method BINGO" { t.Errorf("not expected error message: %s", e) } werr := e.(*internal.ErrorNoResponderFoundWrongMethod).Unwrap() if werr != internal.NoResponderFound { t.Errorf("NoResponderFound is not wrapped, but %[1]s (%[1]T)", werr) } } httpmock-1.1.0/internal/route_key.go000066400000000000000000000003331416056172200175160ustar00rootroot00000000000000package internal type RouteKey struct { Method string URL string } var NoResponder RouteKey func (r RouteKey) String() string { if r == NoResponder { return "NO_RESPONDER" } return r.Method + " " + r.URL } httpmock-1.1.0/internal/route_key_test.go000066400000000000000000000006501416056172200205570ustar00rootroot00000000000000package internal_test import ( "testing" "github.com/jarcoal/httpmock/internal" ) func TestRouteKey(t *testing.T) { got, expected := internal.NoResponder.String(), "NO_RESPONDER" if got != expected { t.Errorf("got: %v, expected: %v", got, expected) } got, expected = internal.RouteKey{Method: "GET", URL: "/foo"}.String(), "GET /foo" if got != expected { t.Errorf("got: %v, expected: %v", got, expected) } } httpmock-1.1.0/internal/stack_tracer.go000066400000000000000000000035441416056172200201640ustar00rootroot00000000000000package internal import ( "bytes" "fmt" "net/http" "runtime" "strings" ) type StackTracer struct { CustomFn func(...interface{}) Err error } // Error implements error interface. func (s StackTracer) Error() string { if s.Err == nil { return "" } return s.Err.Error() } // Unwrap implements the interface needed by errors.Unwrap. func (s StackTracer) Unwrap() error { return s.Err } // CheckStackTracer checks for specific error returned by // NewNotFoundResponder function or Trace Responder method. func CheckStackTracer(req *http.Request, err error) error { if nf, ok := err.(StackTracer); ok { if nf.CustomFn != nil { pc := make([]uintptr, 128) npc := runtime.Callers(2, pc) pc = pc[:npc] var mesg bytes.Buffer var netHTTPBegin, netHTTPEnd bool // Start recording at first net/http call if any... for { frames := runtime.CallersFrames(pc) var lastFn string for { frame, more := frames.Next() if !netHTTPEnd { if netHTTPBegin { netHTTPEnd = !strings.HasPrefix(frame.Function, "net/http.") } else { netHTTPBegin = strings.HasPrefix(frame.Function, "net/http.") } } if netHTTPEnd { if lastFn != "" { if mesg.Len() == 0 { if nf.Err != nil { mesg.WriteString(nf.Err.Error()) } else { fmt.Fprintf(&mesg, "%s %s", req.Method, req.URL) } mesg.WriteString("\nCalled from ") } else { mesg.WriteString("\n ") } fmt.Fprintf(&mesg, "%s()\n at %s:%d", lastFn, frame.File, frame.Line) } } lastFn = frame.Function if !more { break } } // At least one net/http frame found if mesg.Len() > 0 { break } netHTTPEnd = true // retry without looking at net/http frames } nf.CustomFn(mesg.String()) } err = nf.Err } return err } httpmock-1.1.0/internal/stack_tracer_test.go000066400000000000000000000047261416056172200212260ustar00rootroot00000000000000package internal_test import ( "errors" "net/http" "strings" "testing" "github.com/jarcoal/httpmock/internal" ) func TestStackTracer(t *testing.T) { st := internal.StackTracer{} if st.Error() != "" { t.Errorf("Error() returned <%s> instead of <>", st.Error()) } st = internal.StackTracer{ Err: errors.New("foo"), } if st.Error() != "foo" { t.Errorf("Error() returned <%s> instead of ", st.Error()) } if werr := st.Unwrap(); werr != st.Err { t.Errorf("Unwrap() returned <%s> instead of ", werr) } } func TestCheckStackTracer(t *testing.T) { req, err := http.NewRequest("GET", "http://foo.bar/", nil) if err != nil { t.Fatal(err) } // no error gotErr := internal.CheckStackTracer(req, nil) if gotErr != nil { t.Errorf(`CheckStackTracer(nil) should return nil, not %v`, gotErr) } // Classic error err = errors.New("error") gotErr = internal.CheckStackTracer(req, err) if err != gotErr { t.Errorf(`CheckStackTracer(err) should return %v, not %v`, err, gotErr) } // stackTracer without customFn origErr := errors.New("foo") errTracer := internal.StackTracer{ Err: origErr, } gotErr = internal.CheckStackTracer(req, errTracer) if gotErr != origErr { t.Errorf(`Returned error mismatch, expected: %v, got: %v`, origErr, gotErr) } // stackTracer with nil error & without customFn errTracer = internal.StackTracer{} gotErr = internal.CheckStackTracer(req, errTracer) if gotErr != nil { t.Errorf(`Returned error mismatch, expected: nil, got: %v`, gotErr) } // stackTracer var mesg string errTracer = internal.StackTracer{ Err: origErr, CustomFn: func(args ...interface{}) { mesg = args[0].(string) }, } gotErr = internal.CheckStackTracer(req, errTracer) if !strings.HasPrefix(mesg, "foo\nCalled from ") || strings.HasSuffix(mesg, "\n") { t.Errorf(`mesg does not match "^foo\nCalled from .*[^\n]\z", it is "` + mesg + `"`) } if gotErr != origErr { t.Errorf(`Returned error mismatch, expected: %v, got: %v`, origErr, gotErr) } // stackTracer with nil error but customFn mesg = "" errTracer = internal.StackTracer{ CustomFn: func(args ...interface{}) { mesg = args[0].(string) }, } gotErr = internal.CheckStackTracer(req, errTracer) if !strings.HasPrefix(mesg, "GET http://foo.bar/\nCalled from ") || strings.HasSuffix(mesg, "\n") { t.Errorf(`mesg does not match "^foo\nCalled from .*[^\n]\z", it is "` + mesg + `"`) } if gotErr != nil { t.Errorf(`Returned error mismatch, expected: nil, got: %v`, gotErr) } } httpmock-1.1.0/internal/submatches.go000066400000000000000000000006761416056172200176600ustar00rootroot00000000000000package internal import ( "context" "net/http" ) type submatchesKeyType struct{} var submatchesKey submatchesKeyType func SetSubmatches(req *http.Request, submatches []string) *http.Request { if len(submatches) > 0 { return req.WithContext(context.WithValue(req.Context(), submatchesKey, submatches)) } return req } func GetSubmatches(req *http.Request) []string { sm, _ := req.Context().Value(submatchesKey).([]string) return sm } httpmock-1.1.0/internal/submatches_test.go000066400000000000000000000017771416056172200207220ustar00rootroot00000000000000package internal_test import ( "net/http" "testing" "github.com/jarcoal/httpmock/internal" ) func TestSubmatches(t *testing.T) { req, err := http.NewRequest("GET", "/foo/bar", nil) if err != nil { t.Fatal(err) } var req2 *http.Request req2 = internal.SetSubmatches(req, nil) if req2 != req { t.Error("SetSubmatches(req, nil) should return the same request") } sm := internal.GetSubmatches(req2) if sm != nil { t.Errorf("GetSubmatches() should return nil") } req2 = internal.SetSubmatches(req, []string{}) if req2 != req { t.Error("SetSubmatches(req, []string{}) should return the same request") } sm = internal.GetSubmatches(req2) if sm != nil { t.Errorf("GetSubmatches() should return nil") } req2 = internal.SetSubmatches(req, []string{"foo", "123", "-123", "12.3"}) if req2 == req { t.Error("setSubmatches(req, []string{...}) should NOT return the same request") } sm = internal.GetSubmatches(req2) if len(sm) != 4 { t.Errorf("GetSubmatches() should return 4 items") } } httpmock-1.1.0/response.go000066400000000000000000000436361416056172200155470ustar00rootroot00000000000000package httpmock import ( "bytes" "context" "encoding/json" "encoding/xml" "errors" "fmt" "io" "net/http" "reflect" "strconv" "strings" "sync" "time" "github.com/jarcoal/httpmock/internal" ) // fromThenKeyType is used by Then(). type fromThenKeyType struct{} var fromThenKey = fromThenKeyType{} // suggestedMethodKeyType is used by NewNotFoundResponder(). type suggestedMethodKeyType struct{} var suggestedMethodKey = suggestedMethodKeyType{} // Responder is a callback that receives an http request and returns // a mocked response. type Responder func(*http.Request) (*http.Response, error) func (r Responder) times(name string, n int, fn ...func(...interface{})) Responder { count := 0 return func(req *http.Request) (*http.Response, error) { count++ if count > n { err := internal.StackTracer{ Err: fmt.Errorf("Responder not found for %s %s (coz %s and already called %d times)", req.Method, req.URL, name, count), } if len(fn) > 0 { err.CustomFn = fn[0] } return nil, err } return r(req) } } // Times returns a Responder callable n times before returning an // error. If the Responder is called more than n times and fn is // passed and non-nil, it acts as the fn parameter of // NewNotFoundResponder, allowing to dump the stack trace to localize // the origin of the call. // import ( // "testing" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // // This responder is callable 3 times, then an error is returned and // // the stacktrace of the call logged using t.Log() // httpmock.RegisterResponder("GET", "/foo/bar", // httpmock.NewStringResponder(200, "{}").Times(3, t.Log), // ) func (r Responder) Times(n int, fn ...func(...interface{})) Responder { return r.times("Times", n, fn...) } // Once returns a new Responder callable once before returning an // error. If the Responder is called 2 or more times and fn is passed // and non-nil, it acts as the fn parameter of NewNotFoundResponder, // allowing to dump the stack trace to localize the origin of the // call. // import ( // "testing" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // // This responder is callable only once, then an error is returned and // // the stacktrace of the call logged using t.Log() // httpmock.RegisterResponder("GET", "/foo/bar", // httpmock.NewStringResponder(200, "{}").Once(t.Log), // ) func (r Responder) Once(fn ...func(...interface{})) Responder { return r.times("Once", 1, fn...) } // Trace returns a new Responder that allows to easily trace the calls // of the original Responder using fn. It can be used in conjunction // with the testing package as in the example below with the help of // (*testing.T).Log method: // import ( // "testing" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // httpmock.RegisterResponder("GET", "/foo/bar", // httpmock.NewStringResponder(200, "{}").Trace(t.Log), // ) func (r Responder) Trace(fn func(...interface{})) Responder { return func(req *http.Request) (*http.Response, error) { resp, err := r(req) return resp, internal.StackTracer{ CustomFn: fn, Err: err, } } } // Delay returns a new Responder that calls the original r Responder // after a delay of d. // import ( // "testing" // "time" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // httpmock.RegisterResponder("GET", "/foo/bar", // httpmock.NewStringResponder(200, "{}").Delay(100*time.Millisecond), // ) func (r Responder) Delay(d time.Duration) Responder { return func(req *http.Request) (*http.Response, error) { time.Sleep(d) return r(req) } } var errThenDone = errors.New("ThenDone") // similar is simple but a bit tricky. Here we consider two Responder // are similar if they share the same function, but not necessarily // the same environment. It is only used by Then below. func (r Responder) similar(other Responder) bool { return reflect.ValueOf(r).Pointer() == reflect.ValueOf(other).Pointer() } // Then returns a new Responder that calls r on first invocation, then // next on following ones, except when Then is chained, in this case // next is called only once: // A := httpmock.NewStringResponder(200, "A") // B := httpmock.NewStringResponder(200, "B") // C := httpmock.NewStringResponder(200, "C") // // httpmock.RegisterResponder("GET", "/pipo", A.Then(B).Then(C)) // // http.Get("http://foo.bar/pipo") // A is called // http.Get("http://foo.bar/pipo") // B is called // http.Get("http://foo.bar/pipo") // C is called // http.Get("http://foo.bar/pipo") // C is called, and so on // // A panic occurs if next is the result of another Then call (because // allowing it could cause inextricable problems at runtime). Then // calls can be chained, but cannot call each other by // parameter. Example: // A.Then(B).Then(C) // is OK // A.Then(B.Then(C)) // panics as A.Then() parameter is another Then() call func (r Responder) Then(next Responder) (x Responder) { var done int var mu sync.Mutex x = func(req *http.Request) (*http.Response, error) { mu.Lock() defer mu.Unlock() ctx := req.Context() thenCalledUs, _ := ctx.Value(fromThenKey).(bool) if !thenCalledUs { req = req.WithContext(context.WithValue(ctx, fromThenKey, true)) } switch done { case 0: resp, err := r(req) if err != errThenDone { if !x.similar(r) { // r is NOT a Then done = 1 } return resp, err } fallthrough case 1: done = 2 // next is NEVER a Then, as it is forbidden by design return next(req) } if thenCalledUs { return nil, errThenDone } return next(req) } if next.similar(x) { panic("Then() does not accept another Then() Responder as parameter") } return } // ResponderFromResponse wraps an *http.Response in a Responder. // // Be careful, except for responses generated by httpmock // (NewStringResponse and NewBytesResponse functions) for which there // is no problems, it is the caller responsibility to ensure the // response body can be read several times and concurrently if needed, // as it is shared among all Responder returned responses. // // For home-made responses, NewRespBodyFromString and // NewRespBodyFromBytes functions can be used to produce response // bodies that can be read several times and concurrently. func ResponderFromResponse(resp *http.Response) Responder { return func(req *http.Request) (*http.Response, error) { res := *resp // Our stuff: generate a new io.ReadCloser instance sharing the same buffer if body, ok := resp.Body.(*dummyReadCloser); ok { res.Body = body.copy() } res.Request = req return &res, nil } } // ResponderFromMultipleResponses wraps an *http.Response list in a Responder. // // Each response will be returned in the order of the provided list. // If the responder is called more than the size of the provided list, an error // will be thrown. // // Be careful, except for responses generated by httpmock // (NewStringResponse and NewBytesResponse functions) for which there // is no problems, it is the caller responsibility to ensure the // response body can be read several times and concurrently if needed, // as it is shared among all Responder returned responses. // // For home-made responses, NewRespBodyFromString and // NewRespBodyFromBytes functions can be used to produce response // bodies that can be read several times and concurrently. // // If all responses have been returned and fn is passed // and non-nil, it acts as the fn parameter of NewNotFoundResponder, // allowing to dump the stack trace to localize the origin of the // call. // import ( // "github.com/jarcoal/httpmock" // "testing" // ) // ... // func TestMyApp(t *testing.T) { // ... // // This responder is callable only once, then an error is returned and // // the stacktrace of the call logged using t.Log() // httpmock.RegisterResponder("GET", "/foo/bar", // httpmock.ResponderFromMultipleResponses( // []*http.Response{ // httpmock.NewStringResponse(200, `{"name":"bar"}`), // httpmock.NewStringResponse(404, `{"mesg":"Not found"}`), // }, // t.Log), // ) // } func ResponderFromMultipleResponses(responses []*http.Response, fn ...func(...interface{})) Responder { responseIndex := 0 mutex := sync.Mutex{} return func(req *http.Request) (*http.Response, error) { mutex.Lock() defer mutex.Unlock() defer func() { responseIndex++ }() if responseIndex >= len(responses) { err := internal.StackTracer{ Err: fmt.Errorf("not enough responses provided: responder called %d time(s) but %d response(s) provided", responseIndex+1, len(responses)), } if len(fn) > 0 { err.CustomFn = fn[0] } return nil, err } res := *responses[responseIndex] // Our stuff: generate a new io.ReadCloser instance sharing the same buffer if body, ok := responses[responseIndex].Body.(*dummyReadCloser); ok { res.Body = body.copy() } res.Request = req return &res, nil } } // NewErrorResponder creates a Responder that returns an empty request and the // given error. This can be used to e.g. imitate more deep http errors for the // client. func NewErrorResponder(err error) Responder { return func(req *http.Request) (*http.Response, error) { return nil, err } } // NewNotFoundResponder creates a Responder typically used in // conjunction with RegisterNoResponder() function and testing // package, to be proactive when a Responder is not found. fn is // called with a unique string parameter containing the name of the // missing route and the stack trace to localize the origin of the // call. If fn returns (= if it does not panic), the responder returns // an error of the form: "Responder not found for GET http://foo.bar/path". // Note that fn can be nil. // // It is useful when writing tests to ensure that all routes have been // mocked. // // Example of use: // import ( // "testing" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // // Calls testing.Fatal with the name of Responder-less route and // // the stack trace of the call. // httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) // // Will abort the current test and print something like: // transport_test.go:735: Called from net/http.Get() // at /go/src/github.com/jarcoal/httpmock/transport_test.go:714 // github.com/jarcoal/httpmock.TestCheckStackTracer() // at /go/src/testing/testing.go:865 // testing.tRunner() // at /go/src/runtime/asm_amd64.s:1337 func NewNotFoundResponder(fn func(...interface{})) Responder { return func(req *http.Request) (*http.Response, error) { suggestedMethod, _ := req.Context().Value(suggestedMethodKey).(string) if suggestedMethod != "" { suggestedMethod = ", but one matches method " + suggestedMethod } return nil, internal.StackTracer{ CustomFn: fn, Err: fmt.Errorf("Responder not found for %s %s%s", req.Method, req.URL, suggestedMethod), } } } // NewStringResponse creates an *http.Response with a body based on // the given string. Also accepts an http status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewStringResponse(200, httpmock.File("body.txt").String()) func NewStringResponse(status int, body string) *http.Response { return &http.Response{ Status: strconv.Itoa(status), StatusCode: status, Body: NewRespBodyFromString(body), Header: http.Header{}, ContentLength: -1, } } // NewStringResponder creates a Responder from a given body (as a string) and status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewStringResponder(200, httpmock.File("body.txt").String()) func NewStringResponder(status int, body string) Responder { return ResponderFromResponse(NewStringResponse(status, body)) } // NewBytesResponse creates an *http.Response with a body based on the // given bytes. Also accepts an http status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewBytesResponse(200, httpmock.File("body.raw").Bytes()) func NewBytesResponse(status int, body []byte) *http.Response { return &http.Response{ Status: strconv.Itoa(status), StatusCode: status, Body: NewRespBodyFromBytes(body), Header: http.Header{}, ContentLength: -1, } } // NewBytesResponder creates a Responder from a given body (as a byte // slice) and status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewBytesResponder(200, httpmock.File("body.raw").Bytes()) func NewBytesResponder(status int, body []byte) Responder { return ResponderFromResponse(NewBytesResponse(status, body)) } // NewJsonResponse creates an *http.Response with a body that is a // json encoded representation of the given interface{}. Also accepts // an http status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewJsonResponse(200, httpmock.File("body.json")) func NewJsonResponse(status int, body interface{}) (*http.Response, error) { // nolint: revive encoded, err := json.Marshal(body) if err != nil { return nil, err } response := NewBytesResponse(status, encoded) response.Header.Set("Content-Type", "application/json") return response, nil } // NewJsonResponder creates a Responder from a given body (as an // interface{} that is encoded to json) and status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewJsonResponder(200, httpmock.File("body.json")) func NewJsonResponder(status int, body interface{}) (Responder, error) { // nolint: revive resp, err := NewJsonResponse(status, body) if err != nil { return nil, err } return ResponderFromResponse(resp), nil } // NewJsonResponderOrPanic is like NewJsonResponder but panics in case of error. // // It simplifies the call of RegisterResponder, avoiding the use of a // temporary variable and an error check, and so can be used as // NewStringResponder or NewBytesResponder in such context: // httpmock.RegisterResponder( // "GET", // "/test/path", // httpmock.NewJSONResponderOrPanic(200, &MyBody), // ) // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewJsonResponderOrPanic(200, httpmock.File("body.json")) func NewJsonResponderOrPanic(status int, body interface{}) Responder { // nolint: revive responder, err := NewJsonResponder(status, body) if err != nil { panic(err) } return responder } // NewXmlResponse creates an *http.Response with a body that is an xml // encoded representation of the given interface{}. Also accepts an // http status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewXmlResponse(200, httpmock.File("body.xml")) func NewXmlResponse(status int, body interface{}) (*http.Response, error) { // nolint: revive var ( encoded []byte err error ) if f, ok := body.(File); ok { encoded, err = f.bytes() } else { encoded, err = xml.Marshal(body) } if err != nil { return nil, err } response := NewBytesResponse(status, encoded) response.Header.Set("Content-Type", "application/xml") return response, nil } // NewXmlResponder creates a Responder from a given body (as an // interface{} that is encoded to xml) and status code. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewXmlResponder(200, httpmock.File("body.xml")) func NewXmlResponder(status int, body interface{}) (Responder, error) { // nolint: revive resp, err := NewXmlResponse(status, body) if err != nil { return nil, err } return ResponderFromResponse(resp), nil } // NewXmlResponderOrPanic is like NewXmlResponder but panics in case of error. // // It simplifies the call of RegisterResponder, avoiding the use of a // temporary variable and an error check, and so can be used as // NewStringResponder or NewBytesResponder in such context: // httpmock.RegisterResponder( // "GET", // "/test/path", // httpmock.NewXmlResponderOrPanic(200, &MyBody), // ) // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewXmlResponderOrPanic(200, httpmock.File("body.xml")) func NewXmlResponderOrPanic(status int, body interface{}) Responder { // nolint: revive responder, err := NewXmlResponder(status, body) if err != nil { panic(err) } return responder } // NewRespBodyFromString creates an io.ReadCloser from a string that // is suitable for use as an http response body. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewRespBodyFromString(httpmock.File("body.txt").String()) func NewRespBodyFromString(body string) io.ReadCloser { return &dummyReadCloser{orig: body} } // NewRespBodyFromBytes creates an io.ReadCloser from a byte slice // that is suitable for use as an http response body. // // To pass the content of an existing file as body use httpmock.File as in: // httpmock.NewRespBodyFromBytes(httpmock.File("body.txt").Bytes()) func NewRespBodyFromBytes(body []byte) io.ReadCloser { return &dummyReadCloser{orig: body} } type dummyReadCloser struct { orig interface{} // string or []byte body io.ReadSeeker // instanciated on demand from orig } // copy returns a new instance resetting d.body to nil. func (d *dummyReadCloser) copy() *dummyReadCloser { return &dummyReadCloser{orig: d.orig} } // setup ensures d.body is correctly initialized. func (d *dummyReadCloser) setup() { if d.body == nil { switch body := d.orig.(type) { case string: d.body = strings.NewReader(body) case []byte: d.body = bytes.NewReader(body) } } } func (d *dummyReadCloser) Read(p []byte) (n int, err error) { d.setup() return d.body.Read(p) } func (d *dummyReadCloser) Close() error { d.setup() d.body.Seek(0, io.SeekEnd) // nolint: errcheck return nil } httpmock-1.1.0/response_test.go000066400000000000000000000442261416056172200166020ustar00rootroot00000000000000package httpmock_test import ( "encoding/xml" "errors" "fmt" "io" "io/ioutil" "net/http" "path/filepath" "strings" "sync" "testing" "time" . "github.com/jarcoal/httpmock" "github.com/jarcoal/httpmock/internal" ) func TestResponderFromResponse(t *testing.T) { responder := ResponderFromResponse(NewStringResponse(200, "hello world")) req, err := http.NewRequest(http.MethodGet, testURL, nil) if err != nil { t.Fatal("Error creating request") } response1, err := responder(req) if err != nil { t.Error("Error should be nil") } testURLWithQuery := testURL + "?a=1" req, err = http.NewRequest(http.MethodGet, testURLWithQuery, nil) if err != nil { t.Fatal("Error creating request") } response2, err := responder(req) if err != nil { t.Error("Error should be nil") } // Body should be the same for both responses assertBody(t, response1, "hello world") assertBody(t, response2, "hello world") // Request should be non-nil and different for each response if response1.Request != nil && response2.Request != nil { if response1.Request.URL.String() != testURL { t.Errorf("Expected request url %s, got: %s", testURL, response1.Request.URL.String()) } if response2.Request.URL.String() != testURLWithQuery { t.Errorf("Expected request url %s, got: %s", testURLWithQuery, response2.Request.URL.String()) } } else { t.Error("response.Request should not be nil") } } func TestResponderFromResponses(t *testing.T) { jsonResponse, err := NewJsonResponse(200, map[string]string{"test": "toto"}) if err != nil { t.Errorf("NewJsonResponse failed: %s", err) } responder := ResponderFromMultipleResponses( []*http.Response{ jsonResponse, NewStringResponse(200, "hello world"), }, ) req, err := http.NewRequest(http.MethodGet, testURL, nil) if err != nil { t.Fatal("Error creating request") } response1, err := responder(req) if err != nil { t.Error("Error should be nil") } testURLWithQuery := testURL + "?a=1" req, err = http.NewRequest(http.MethodGet, testURLWithQuery, nil) if err != nil { t.Fatal("Error creating request") } response2, err := responder(req) if err != nil { t.Error("Error should be nil") } // Body should be the same for both responses assertBody(t, response1, `{"test":"toto"}`) assertBody(t, response2, "hello world") // Request should be non-nil and different for each response if response1.Request != nil && response2.Request != nil { if response1.Request.URL.String() != testURL { t.Errorf("Expected request url %s, got: %s", testURL, response1.Request.URL.String()) } if response2.Request.URL.String() != testURLWithQuery { t.Errorf("Expected request url %s, got: %s", testURLWithQuery, response2.Request.URL.String()) } } else { t.Error("response.Request should not be nil") } // ensure we can't call the responder more than the number of responses it embeds _, err = responder(req) if err == nil { t.Error("Error should not be nil") } else if err.Error() != "not enough responses provided: responder called 3 time(s) but 2 response(s) provided" { t.Error("Invalid error message") } // fn usage responder = ResponderFromMultipleResponses([]*http.Response{}, func(args ...interface{}) {}) _, err = responder(req) if err == nil { t.Error("Error should not be nil") } else if err.Error() != "not enough responses provided: responder called 1 time(s) but 0 response(s) provided" { t.Errorf("Invalid error message") } else if ne, ok := err.(internal.StackTracer); !ok { t.Errorf(`err type mismatch, got %T, expected internal.StackTracer`, err) } else if ne.CustomFn == nil { t.Error(`ne.CustomFn should not be nil`) } } func TestNewNotFoundResponder(t *testing.T) { responder := NewNotFoundResponder(func(args ...interface{}) {}) req, err := http.NewRequest("GET", "http://foo.bar/path", nil) if err != nil { t.Fatal("Error creating request") } const title = "Responder not found for GET http://foo.bar/path" resp, err := responder(req) if resp != nil { t.Error("resp should be nil") } if err == nil { t.Error("err should be not nil") } else if err.Error() != title { t.Errorf(`err mismatch, got: "%s", expected: "%s"`, err, "Responder not found for: GET http://foo.bar/path") } else if ne, ok := err.(internal.StackTracer); !ok { t.Errorf(`err type mismatch, got %T, expected httpmock.notFound`, err) } else if ne.CustomFn == nil { t.Error(`err CustomFn mismatch, got: nil, expected: non-nil`) } // nil fn responder = NewNotFoundResponder(nil) resp, err = responder(req) if resp != nil { t.Error("resp should be nil") } if err == nil { t.Error("err should be not nil") } else if err.Error() != title { t.Errorf(`err mismatch, got: "%s", expected: "%s"`, err, "Responder not found for: GET http://foo.bar/path") } else if ne, ok := err.(internal.StackTracer); !ok { t.Errorf(`err type mismatch, got %T, expected httpmock.notFound`, err) } else if ne.CustomFn != nil { t.Errorf(`err CustomFn mismatch, got: %p, expected: nil`, ne.CustomFn) } } func TestNewStringResponse(t *testing.T) { body := "hello world" status := 200 response := NewStringResponse(status, body) data, err := ioutil.ReadAll(response.Body) if err != nil { t.Fatal(err) } if string(data) != body { t.FailNow() } if response.StatusCode != status { t.FailNow() } } func TestNewBytesResponse(t *testing.T) { body := []byte("hello world") status := 200 response := NewBytesResponse(status, body) data, err := ioutil.ReadAll(response.Body) if err != nil { t.Fatal(err) } if string(data) != string(body) { t.FailNow() } if response.StatusCode != status { t.FailNow() } } func TestNewJsonResponse(t *testing.T) { type schema struct { Hello string `json:"hello"` } dir, cleanup := tmpDir(t) defer cleanup() fileName := filepath.Join(dir, "ok.json") writeFile(t, fileName, []byte(`{ "test": true }`)) for i, test := range []struct { body interface{} expected string }{ {body: &schema{"world"}, expected: `{"hello":"world"}`}, {body: File(fileName), expected: `{"test":true}`}, } { response, err := NewJsonResponse(200, test.body) if err != nil { t.Errorf("#%d NewJsonResponse failed: %s", i, err) continue } if response.StatusCode != 200 { t.Errorf("#%d response status mismatch: %d ≠ 200", i, response.StatusCode) continue } if response.Header.Get("Content-Type") != "application/json" { t.Errorf("#%d response Content-Type mismatch: %s ≠ application/json", i, response.Header.Get("Content-Type")) continue } assertBody(t, response, test.expected) } // Error case response, err := NewJsonResponse(200, func() {}) if response != nil { t.Fatal("response is not nil") } if err == nil { t.Fatal("no error occurred") } } func checkResponder(t *testing.T, r Responder, expectedStatus int, expectedBody string) { helper(t).Helper() req, _ := http.NewRequest(http.MethodGet, "/foo", nil) resp, err := r(req) if err != nil { t.Errorf("An error occurred: %s", err) return } if resp == nil { t.Error("Responder returned a nil response") return } if resp.StatusCode != expectedStatus { t.Errorf("Status code mismatch: got=%d expected=%d", resp.StatusCode, expectedStatus) } assertBody(t, resp, expectedBody) } func TestNewJsonResponder(t *testing.T) { t.Run("OK", func(t *testing.T) { r, err := NewJsonResponder(200, map[string]int{"foo": 42}) if err != nil { t.Error(err) return } checkResponder(t, r, 200, `{"foo":42}`) }) t.Run("OK file", func(t *testing.T) { dir, cleanup := tmpDir(t) defer cleanup() fileName := filepath.Join(dir, "ok.json") writeFile(t, fileName, []byte(`{ "foo" : 42 }`)) r, err := NewJsonResponder(200, File(fileName)) if err != nil { t.Error(err) return } checkResponder(t, r, 200, `{"foo":42}`) }) t.Run("Error", func(t *testing.T) { r, err := NewJsonResponder(200, func() {}) if r != nil { t.Error("responder is not nil") } if err == nil { t.Error("no error occurred") } }) t.Run("OK don't panic", func(t *testing.T) { panicked, str := catchPanic( func() { r := NewJsonResponderOrPanic(200, map[string]int{"foo": 42}) checkResponder(t, r, 200, `{"foo":42}`) }, ) if panicked { t.Errorf("A panic occurred: <%s>", str) } }) t.Run("Panic", func(t *testing.T) { panicked, _ := catchPanic( func() { NewJsonResponderOrPanic(200, func() {}) }, ) if !panicked { t.Error("no panic occurred") } }) } type schemaXML struct { Hello string `xml:"hello"` } func TestNewXmlResponse(t *testing.T) { body := &schemaXML{"world"} b, err := xml.Marshal(body) if err != nil { t.Fatalf("Cannot xml.Marshal expected body: %s", err) } expectedBody := string(b) dir, cleanup := tmpDir(t) defer cleanup() fileName := filepath.Join(dir, "ok.xml") writeFile(t, fileName, b) for i, test := range []struct { body interface{} expected string }{ {body: body, expected: expectedBody}, {body: File(fileName), expected: expectedBody}, } { response, err := NewXmlResponse(200, test.body) if err != nil { t.Errorf("#%d NewXmlResponse failed: %s", i, err) continue } if response.StatusCode != 200 { t.Errorf("#%d response status mismatch: %d ≠ 200", i, response.StatusCode) continue } if response.Header.Get("Content-Type") != "application/xml" { t.Errorf("#%d response Content-Type mismatch: %s ≠ application/xml", i, response.Header.Get("Content-Type")) continue } assertBody(t, response, test.expected) } // Error case response, err := NewXmlResponse(200, func() {}) if response != nil { t.Fatal("response is not nil") } if err == nil { t.Fatal("no error occurred") } } func TestNewXmlResponder(t *testing.T) { body := &schemaXML{"world"} b, err := xml.Marshal(body) if err != nil { t.Fatalf("Cannot xml.Marshal expected body: %s", err) } expectedBody := string(b) t.Run("OK", func(t *testing.T) { r, err := NewXmlResponder(200, body) if err != nil { t.Error(err) return } checkResponder(t, r, 200, expectedBody) }) t.Run("OK file", func(t *testing.T) { dir, cleanup := tmpDir(t) defer cleanup() fileName := filepath.Join(dir, "ok.xml") writeFile(t, fileName, b) r, err := NewXmlResponder(200, File(fileName)) if err != nil { t.Error(err) return } checkResponder(t, r, 200, expectedBody) }) t.Run("Error", func(t *testing.T) { r, err := NewXmlResponder(200, func() {}) if r != nil { t.Error("responder is not nil") } if err == nil { t.Error("no error occurred") } }) t.Run("OK don't panic", func(t *testing.T) { panicked, str := catchPanic( func() { r := NewXmlResponderOrPanic(200, body) checkResponder(t, r, 200, expectedBody) }, ) if panicked { t.Errorf("A panic occurred: <%s>", str) } }) t.Run("Panic", func(t *testing.T) { panicked, _ := catchPanic( func() { NewXmlResponderOrPanic(200, func() {}) }, ) if !panicked { t.Error("no panic occurred") } }) } func TestNewErrorResponder(t *testing.T) { // From go1.13, a stack frame is stored into errors issued by errors.New() origError := errors.New("oh no") responder := NewErrorResponder(origError) req, err := http.NewRequest(http.MethodGet, testURL, nil) if err != nil { t.Fatal("Error creating request") } response, err := responder(req) if response != nil { t.Error("Response should be nil") } if err != origError { t.Errorf("Expected error %#v, got: %#v", origError, err) } } func TestResponseBody(t *testing.T) { const ( body = "hello world" status = 200 ) t.Run("http.Response", func(t *testing.T) { for i, response := range []*http.Response{ NewBytesResponse(status, []byte(body)), NewStringResponse(status, body), } { t.Run(fmt.Sprintf("resp #%d", i), func(t *testing.T) { data, err := ioutil.ReadAll(response.Body) if err != nil { t.Error(err) return } if string(data) != string(body) { t.Errorf("body mismatch: %q ≠ %q", data, body) return } if response.StatusCode != status { t.Errorf("status mismatch: %d ≠ %d", response.StatusCode, status) return } var buf [1]byte _, err = response.Body.Read(buf[:]) if err == nil { t.Errorf("Next Read() should produce an error") return } if err != io.EOF { t.Errorf("Next Read() should io.EOF") } }) } }) t.Run("Responder", func(t *testing.T) { for i, responder := range []Responder{ NewBytesResponder(200, []byte(body)), NewStringResponder(200, body), } { t.Run(fmt.Sprintf("resp #%d", i), func(t *testing.T) { req, _ := http.NewRequest("GET", "http://foo.bar", nil) response, err := responder(req) if err != nil { t.Error(err) return } data, err := ioutil.ReadAll(response.Body) if err != nil { t.Error(err) return } if string(data) != string(body) { t.Errorf("body mismatch: %q ≠ %q", data, body) return } var buf [1]byte _, err = response.Body.Read(buf[:]) if err == nil { t.Errorf("Next Read() should produce an error") return } if err != io.EOF { t.Errorf("Next Read() should io.EOF") } }) } }) } func TestResponder(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) if err != nil { t.Fatal("Error creating request") } resp := &http.Response{} chk := func(r Responder, expectedResp *http.Response, expectedErr string) { helper(t).Helper() gotResp, gotErr := r(req) if gotResp != expectedResp { t.Errorf(`Response mismatch, expected: %v, got: %v`, expectedResp, gotResp) } var gotErrStr string if gotErr != nil { gotErrStr = gotErr.Error() } if gotErrStr != expectedErr { t.Errorf(`Error mismatch, expected: %v, got: %v`, expectedErr, gotErrStr) } } called := false chkNotCalled := func() { if called { helper(t).Helper() t.Errorf("Original responder should not be called") called = false } } chkCalled := func() { if !called { helper(t).Helper() t.Errorf("Original responder should be called") } called = false } r := Responder(func(*http.Request) (*http.Response, error) { called = true return resp, nil }) chk(r, resp, "") chkCalled() // // Once ro := r.Once() chk(ro, resp, "") chkCalled() chk(ro, nil, "Responder not found for GET http://foo.bar (coz Once and already called 2 times)") chkNotCalled() chk(ro, nil, "Responder not found for GET http://foo.bar (coz Once and already called 3 times)") chkNotCalled() ro = r.Once(func(args ...interface{}) {}) chk(ro, resp, "") chkCalled() chk(ro, nil, "Responder not found for GET http://foo.bar (coz Once and already called 2 times)") chkNotCalled() // // Times rt := r.Times(2) chk(rt, resp, "") chkCalled() chk(rt, resp, "") chkCalled() chk(rt, nil, "Responder not found for GET http://foo.bar (coz Times and already called 3 times)") chkNotCalled() chk(rt, nil, "Responder not found for GET http://foo.bar (coz Times and already called 4 times)") chkNotCalled() rt = r.Times(1, func(args ...interface{}) {}) chk(rt, resp, "") chkCalled() chk(rt, nil, "Responder not found for GET http://foo.bar (coz Times and already called 2 times)") chkNotCalled() // // Trace rt = r.Trace(func(args ...interface{}) {}) chk(rt, resp, "") chkCalled() chk(rt, resp, "") chkCalled() // // Delay rt = r.Delay(100 * time.Millisecond) before := time.Now() chk(rt, resp, "") duration := time.Since(before) chkCalled() if duration < 100*time.Millisecond { t.Errorf("Responder is not delayed, only %s elapsed", duration) } } func TestResponder_Then(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) if err != nil { t.Fatalf("Error creating request: %s", err) } // // Then var stack string newResponder := func(level string) Responder { return func(*http.Request) (*http.Response, error) { stack += level return NewStringResponse(200, level), nil } } var rt Responder chk := func(t *testing.T, expectedLevel, expectedStack string) { helper(t).Helper() resp, err := rt(req) if err != nil { t.Errorf("Responder retruned an unexpected error: %s", err) return } b, err := ioutil.ReadAll(resp.Body) if err != nil { t.Errorf("Read response failed: %s", err) return } if string(b) != expectedLevel { t.Errorf("level: got %q but expected %q (stack is %q)", b, expectedLevel, stack) return } if stack != expectedStack { t.Errorf("stack: got %q but expected %q", stack, expectedStack) return } } A, B, C := newResponder("A"), newResponder("B"), newResponder("C") D, E, F := newResponder("D"), newResponder("E"), newResponder("F") t.Run("simple", func(t *testing.T) { // (r=A,then=B) rt = A.Then(B) chk(t, "A", "A") chk(t, "B", "AB") chk(t, "B", "ABB") chk(t, "B", "ABBB") }) stack = "" t.Run("simple chained", func(t *testing.T) { // (r=A,then=B) // (r=↑,then=C) // (r=↑,then=D) // (r=↑,then=E) // (r=↑,then=F) rt = A.Then(B). Then(C). Then(D). Then(E). Then(F) chk(t, "A", "A") chk(t, "B", "AB") chk(t, "C", "ABC") chk(t, "D", "ABCD") chk(t, "E", "ABCDE") chk(t, "F", "ABCDEF") chk(t, "F", "ABCDEFF") chk(t, "F", "ABCDEFFF") }) stack = "" t.Run("Then Responder as Then param", func(t *testing.T) { panicked, str := catchPanic(func() { A.Then(B.Then(C)) }) if str != "Then() does not accept another Then() Responder as parameter" { if !panicked { t.Error("Should have panicked") } else { t.Errorf("Wrong panic message: %q", str) } } }) } func TestParallelResponder(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) if err != nil { t.Fatal("Error creating request") } body := strings.Repeat("ABC-", 1000) for _, r := range []Responder{ NewStringResponder(200, body), NewBytesResponder(200, []byte(body)), } { var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() resp, _ := r(req) b, err := ioutil.ReadAll(resp.Body) switch { case err != nil: t.Errorf("ReadAll error: %s", err) case len(b) != 4000: t.Errorf("ReadAll read only %d bytes", len(b)) case !strings.HasPrefix(string(b), "ABC-"): t.Errorf("ReadAll does not read the right prefix: %s", string(b)[0:4]) } }() } wg.Wait() } } httpmock-1.1.0/transport.go000066400000000000000000001155161416056172200157420ustar00rootroot00000000000000package httpmock import ( "bytes" "context" "errors" "fmt" "net/http" "net/url" "regexp" "sort" "strconv" "strings" "sync" "github.com/jarcoal/httpmock/internal" ) const regexpPrefix = "=~" // NoResponderFound is returned when no responders are found for a // given HTTP method and URL. var NoResponderFound = internal.NoResponderFound var stdMethods = map[string]bool{ "CONNECT": true, // Section 9.9 "DELETE": true, // Section 9.7 "GET": true, // Section 9.3 "HEAD": true, // Section 9.4 "OPTIONS": true, // Section 9.2 "POST": true, // Section 9.5 "PUT": true, // Section 9.6 "TRACE": true, // Section 9.8 } // methodProbablyWrong returns true if method has probably wrong case. func methodProbablyWrong(method string) bool { return !stdMethods[method] && stdMethods[strings.ToUpper(method)] } // ConnectionFailure is a responder that returns a connection failure. // This is the default responder, and is called when no other matching // responder is found. func ConnectionFailure(*http.Request) (*http.Response, error) { return nil, NoResponderFound } // NewMockTransport creates a new *MockTransport with no responders. func NewMockTransport() *MockTransport { return &MockTransport{ responders: make(map[internal.RouteKey]Responder), callCountInfo: make(map[internal.RouteKey]int), } } type regexpResponder struct { origRx string method string rx *regexp.Regexp responder Responder } // MockTransport implements http.RoundTripper, which fulfills single // http requests issued by an http.Client. This implementation // doesn't actually make the call, instead deferring to the registered // list of responders. type MockTransport struct { // DontCheckMethod disables standard methods check. By default, if // a responder is registered using a lower-cased method among CONNECT, // DELETE, GET, HEAD, OPTIONS, POST, PUT and TRACE, a panic occurs // as it is probably a mistake. DontCheckMethod bool mu sync.RWMutex responders map[internal.RouteKey]Responder regexpResponders []regexpResponder noResponder Responder callCountInfo map[internal.RouteKey]int totalCallCount int } func (m *MockTransport) findResponder(method string, url *url.URL) ( responder Responder, key, respKey internal.RouteKey, submatches []string, ) { urlStr := url.String() key = internal.RouteKey{ Method: method, } for _, getResponder := range []func(internal.RouteKey) (Responder, internal.RouteKey, []string){ m.responderForKey, // Exact match m.regexpResponderForKey, // Regexp match } { // try and get a responder that matches the method and URL with // query params untouched: http://z.tld/path?q... key.URL = urlStr responder, respKey, submatches = getResponder(key) if responder != nil { break } // if we weren't able to find a responder, try with the URL *and* // sorted query params query := sortedQuery(url.Query()) if query != "" { // Replace unsorted query params by sorted ones: // http://z.tld/path?sorted_q... key.URL = strings.Replace(urlStr, url.RawQuery, query, 1) responder, respKey, submatches = getResponder(key) if responder != nil { break } } // if we weren't able to find a responder, try without any query params strippedURL := *url strippedURL.RawQuery = "" strippedURL.Fragment = "" // go1.6 does not handle URL.ForceQuery, so in case it is set in go>1.6, // remove the "?" manually if present. surl := strings.TrimSuffix(strippedURL.String(), "?") hasQueryString := urlStr != surl // if the URL contains a querystring then we strip off the // querystring and try again: http://z.tld/path if hasQueryString { key.URL = surl responder, respKey, submatches = getResponder(key) if responder != nil { break } } // if we weren't able to find a responder for the full URL, try with // the path part only pathAlone := url.Path // First with unsorted querystring: /path?q... if hasQueryString { key.URL = pathAlone + strings.TrimPrefix(urlStr, surl) // concat after-path part responder, respKey, submatches = getResponder(key) if responder != nil { break } // Then with sorted querystring: /path?sorted_q... key.URL = pathAlone + "?" + sortedQuery(url.Query()) if url.Fragment != "" { key.URL += "#" + url.Fragment } responder, respKey, submatches = getResponder(key) if responder != nil { break } } // Then using path alone: /path key.URL = pathAlone responder, respKey, submatches = getResponder(key) if responder != nil { break } } return } // RoundTrip receives HTTP requests and routes them to the appropriate // responder. It is required to implement the http.RoundTripper // interface. You will not interact with this directly, instead the // *http.Client you are using will call it for you. func (m *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) { method := req.Method if method == "" { // http.Request.Method is documented to default to GET: method = http.MethodGet } var suggestedMethod string responder, key, respKey, submatches := m.findResponder(method, req.URL) if responder == nil { // Responder not found, try to detect some common user mistakes on method var altResp Responder var altKey internal.RouteKey if methodProbablyWrong(method) { // Get → GET altResp, _, altKey, _ = m.findResponder(strings.ToUpper(method), req.URL) } if altResp == nil { // Search for any other method altResp, _, altKey, _ = m.findResponder("", req.URL) } if altResp != nil { suggestedMethod = altKey.Method } } m.mu.Lock() // if we found a responder, call it if responder != nil { m.callCountInfo[key]++ if key != respKey { m.callCountInfo[respKey]++ } m.totalCallCount++ } else if m.noResponder != nil { // we didn't find a responder, so fire the 'no responder' responder m.callCountInfo[internal.NoResponder]++ m.totalCallCount++ // give a hint to NewNotFoundResponder() if it is a possible method error if suggestedMethod != "" { req = req.WithContext(context.WithValue(req.Context(), suggestedMethodKey, suggestedMethod)) } responder = m.noResponder } m.mu.Unlock() if responder == nil { if suggestedMethod != "" { return nil, internal.NewErrorNoResponderFoundWrongMethod(method, suggestedMethod) } return ConnectionFailure(req) } return runCancelable(responder, internal.SetSubmatches(req, submatches)) } // NumResponders returns the number of responders currently in use. func (m *MockTransport) NumResponders() int { m.mu.RLock() defer m.mu.RUnlock() return len(m.responders) + len(m.regexpResponders) } func runCancelable(responder Responder, req *http.Request) (*http.Response, error) { ctx := req.Context() if req.Cancel == nil && ctx.Done() == nil { // nolint: staticcheck resp, err := responder(req) return resp, internal.CheckStackTracer(req, err) } // Set up a goroutine that translates a close(req.Cancel) into a // "request canceled" error, and another one that runs the // responder. Then race them: first to the result channel wins. type result struct { response *http.Response err error } resultch := make(chan result, 1) done := make(chan struct{}, 1) go func() { select { case <-req.Cancel: // nolint: staticcheck resultch <- result{ response: nil, err: errors.New("request canceled"), } case <-ctx.Done(): resultch <- result{ response: nil, err: ctx.Err(), } case <-done: } }() go func() { defer func() { if err := recover(); err != nil { resultch <- result{ response: nil, err: fmt.Errorf("panic in responder: got %q", err), } } }() response, err := responder(req) resultch <- result{ response: response, err: err, } }() r := <-resultch // if a cancel() issued from context.WithCancel() or a // close(req.Cancel) are never coming, we'll need to unblock the // first goroutine. done <- struct{}{} return r.response, internal.CheckStackTracer(req, r.err) } // responderForKey returns a responder for a given key. func (m *MockTransport) responderForKey(key internal.RouteKey) (Responder, internal.RouteKey, []string) { m.mu.RLock() defer m.mu.RUnlock() if key.Method != "" { return m.responders[key], key, nil } for k, resp := range m.responders { if key.URL == k.URL { return resp, k, nil } } return nil, key, nil } // responderForKeyUsingRegexp returns the first responder matching a // given key using regexps. func (m *MockTransport) regexpResponderForKey(key internal.RouteKey) (Responder, internal.RouteKey, []string) { m.mu.RLock() defer m.mu.RUnlock() for _, regInfo := range m.regexpResponders { if key.Method == "" || regInfo.method == key.Method { if sm := regInfo.rx.FindStringSubmatch(key.URL); sm != nil { if len(sm) == 1 { sm = nil } else { sm = sm[1:] } return regInfo.responder, internal.RouteKey{ Method: regInfo.method, URL: regInfo.origRx, }, sm } } } return nil, key, nil } func isRegexpURL(url string) bool { return strings.HasPrefix(url, regexpPrefix) } func (m *MockTransport) checkMethod(method string) { if !m.DontCheckMethod && methodProbablyWrong(method) { 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", strings.ToUpper(method), method, )) } } // RegisterResponder adds a new responder, associated with a given // HTTP method and URL (or path). // // When a request comes in that matches, the responder is called and // the response returned to the client. // // If url contains query parameters, their order matters as well as // their content. All following URLs are here considered as different: // http://z.tld?a=1&b=1 // http://z.tld?b=1&a=1 // http://z.tld?a&b // http://z.tld?a=&b= // // If url begins with "=~", the following chars are considered as a // regular expression. If this regexp can not be compiled, it panics. // Note that the "=~" prefix remains in statistics returned by // GetCallCountInfo(). As 2 regexps can match the same URL, the regexp // responders are tested in the order they are registered. Registering // an already existing regexp responder (same method & same regexp // string) replaces its responder, but does not change its position. // // Registering an already existing responder resets the corresponding // statistics as returned by GetCallCountInfo(). // // Registering a nil Responder removes the existing one and the // corresponding statistics as returned by GetCallCountInfo(). It does // nothing if it does not already exist. // // See RegisterRegexpResponder() to directly pass a *regexp.Regexp. // // Example: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // httpmock.RegisterResponder("GET", "http://example.com/", // httpmock.NewStringResponder(200, "hello world")) // // httpmock.RegisterResponder("GET", "/path/only", // httpmock.NewStringResponder("any host hello world", 200)) // // httpmock.RegisterResponder("GET", `=~^/item/id/\d+\z`, // httpmock.NewStringResponder("any item get", 200)) // // // requests to http://example.com/ now return "hello world" and // // requests to any host with path /path/only return "any host hello world" // // requests to any host with path matching ^/item/id/\d+\z regular expression return "any item get" // } // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. func (m *MockTransport) RegisterResponder(method, url string, responder Responder) { m.checkMethod(method) if isRegexpURL(url) { m.registerRegexpResponder(regexpResponder{ origRx: url, method: method, rx: regexp.MustCompile(url[2:]), responder: responder, }) return } key := internal.RouteKey{ Method: method, URL: url, } m.mu.Lock() if responder == nil { delete(m.responders, key) delete(m.callCountInfo, key) } else { m.responders[key] = responder m.callCountInfo[key] = 0 } m.mu.Unlock() } func (m *MockTransport) registerRegexpResponder(rxResp regexpResponder) { m.mu.Lock() defer m.mu.Unlock() found: for { for i, rr := range m.regexpResponders { if rr.method == rxResp.method && rr.origRx == rxResp.origRx { if rxResp.responder == nil { copy(m.regexpResponders[:i], m.regexpResponders[i+1:]) m.regexpResponders[len(m.regexpResponders)-1] = regexpResponder{} m.regexpResponders = m.regexpResponders[:len(m.regexpResponders)-1] } else { m.regexpResponders[i] = rxResp } break found } } if rxResp.responder != nil { m.regexpResponders = append(m.regexpResponders, rxResp) } break // nolint: staticcheck } key := internal.RouteKey{ Method: rxResp.method, URL: rxResp.origRx, } if rxResp.responder == nil { delete(m.callCountInfo, key) } else { m.callCountInfo[key] = 0 } } // RegisterRegexpResponder adds a new responder, associated with a given // HTTP method and URL (or path) regular expression. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // As 2 regexps can match the same URL, the regexp responders are // tested in the order they are registered. Registering an already // existing regexp responder (same method & same regexp string) // replaces its responder, but does not change its position, and // resets the corresponding statistics as returned by GetCallCountInfo(). // // Registering a nil Responder removes the existing one and the // corresponding statistics as returned by GetCallCountInfo(). It does // nothing if it does not already exist. // // A "=~" prefix is added to the stringified regexp in the statistics // returned by GetCallCountInfo(). // // See RegisterResponder function and the "=~" prefix in its url // parameter to avoid compiling the regexp by yourself. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. func (m *MockTransport) RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { m.checkMethod(method) m.registerRegexpResponder(regexpResponder{ origRx: regexpPrefix + urlRegexp.String(), method: method, rx: urlRegexp, responder: responder, }) } // RegisterResponderWithQuery is same as RegisterResponder, but it // doesn't depend on query items order. // // If query is non-nil, its type can be: // url.Values // map[string]string // string, a query string like "a=12&a=13&b=z&c" (see net/url.ParseQuery function) // // If the query type is not recognized or the string cannot be parsed // using net/url.ParseQuery, a panic() occurs. // // Unlike RegisterResponder, path cannot be prefixed by "=~" to say it // is a regexp. If it is, a panic occurs. // // Registering an already existing responder resets the corresponding // statistics as returned by GetCallCountInfo(). // // Registering a nil Responder removes the existing one and the // corresponding statistics as returned by GetCallCountInfo(). It does // nothing if it does not already exist. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting m.DontCheckMethod to // true prior to this call. func (m *MockTransport) RegisterResponderWithQuery(method, path string, query interface{}, responder Responder) { if isRegexpURL(path) { panic(`path begins with "=~", RegisterResponder should be used instead of RegisterResponderWithQuery`) } var mapQuery url.Values switch q := query.(type) { case url.Values: mapQuery = q case map[string]string: mapQuery = make(url.Values, len(q)) for key, e := range q { mapQuery[key] = []string{e} } case string: var err error mapQuery, err = url.ParseQuery(q) if err != nil { panic("RegisterResponderWithQuery bad query string: " + err.Error()) } default: if query != nil { panic(fmt.Sprintf("RegisterResponderWithQuery bad query type %T. Only url.Values, map[string]string and string are allowed", query)) } } if queryString := sortedQuery(mapQuery); queryString != "" { path += "?" + queryString } m.RegisterResponder(method, path, responder) } func sortedQuery(m url.Values) string { if len(m) == 0 { return "" } keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } sort.Strings(keys) var b bytes.Buffer var values []string // nolint: prealloc for _, k := range keys { // Do not alter the passed url.Values values = append(values, m[k]...) sort.Strings(values) k = url.QueryEscape(k) for _, v := range values { if b.Len() > 0 { b.WriteByte('&') } fmt.Fprintf(&b, "%v=%v", k, url.QueryEscape(v)) } values = values[:0] } return b.String() } // RegisterNoResponder is used to register a responder that is called // if no other responder is found. The default is httpmock.ConnectionFailure // that returns an error able to indicate a possible method mismatch. // // Use it in conjunction with NewNotFoundResponder to ensure that all // routes have been mocked: // // import ( // "testing" // "github.com/jarcoal/httpmock" // ) // ... // func TestMyApp(t *testing.T) { // ... // // Calls testing.Fatal with the name of Responder-less route and // // the stack trace of the call. // httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal)) // // Will abort the current test and print something like: // transport_test.go:735: Called from net/http.Get() // at /go/src/github.com/jarcoal/httpmock/transport_test.go:714 // github.com/jarcoal/httpmock.TestCheckStackTracer() // at /go/src/testing/testing.go:865 // testing.tRunner() // at /go/src/runtime/asm_amd64.s:1337 // // If responder is passed as nil, the default behavior // (httpmock.ConnectionFailure) is re-enabled. func (m *MockTransport) RegisterNoResponder(responder Responder) { m.mu.Lock() m.noResponder = responder m.mu.Unlock() } // Reset removes all registered responders (including the no // responder) from the MockTransport. It zeroes call counters too. func (m *MockTransport) Reset() { m.mu.Lock() m.responders = make(map[internal.RouteKey]Responder) m.regexpResponders = nil m.noResponder = nil m.callCountInfo = make(map[internal.RouteKey]int) m.totalCallCount = 0 m.mu.Unlock() } // ZeroCallCounters zeroes call counters without touching registered responders. func (m *MockTransport) ZeroCallCounters() { m.mu.Lock() for k := range m.callCountInfo { m.callCountInfo[k] = 0 } m.totalCallCount = 0 m.mu.Unlock() } // GetCallCountInfo gets the info on all the calls httpmock has caught // since it was activated or reset. The info is returned as a map of // the calling keys with the number of calls made to them as their // value. The key is the method, a space, and the url all concatenated // together. // // As a special case, regexp responders generate 2 entries for each // call. One for the call caught and the other for the rule that // matched. For example: // RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body")) // http.Get("http://z.com") // // will generate the following result: // map[string]int{ // `GET http://z.com`: 1, // `GET =~z\.com\z`: 1, // } func (m *MockTransport) GetCallCountInfo() map[string]int { m.mu.RLock() res := make(map[string]int, len(m.callCountInfo)) for k, v := range m.callCountInfo { res[k.String()] = v } m.mu.RUnlock() return res } // GetTotalCallCount returns the totalCallCount. func (m *MockTransport) GetTotalCallCount() int { m.mu.RLock() count := m.totalCallCount m.mu.RUnlock() return count } // DefaultTransport is the default mock transport used by Activate, // Deactivate, Reset, DeactivateAndReset, RegisterResponder, and // RegisterNoResponder. var DefaultTransport = NewMockTransport() // InitialTransport is a cache of the original transport used so we // can put it back when Deactivate is called. var InitialTransport = http.DefaultTransport // oldClients is used to handle custom http clients (i.e clients other // than http.DefaultClient). var oldClients = map[*http.Client]http.RoundTripper{} // Activate starts the mock environment. This should be called before // your tests run. Under the hood this replaces the Transport on the // http.DefaultClient with httpmock.DefaultTransport. // // To enable mocks for a test, simply activate at the beginning of a test: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // // all http requests using http.DefaultTransport will now be intercepted // } // // If you want all of your tests in a package to be mocked, just call // Activate from init(): // func init() { // httpmock.Activate() // } // // or using a TestMain function: // func TestMain(m *testing.M) { // httpmock.Activate() // os.Exit(m.Run()) // } func Activate() { if Disabled() { return } // make sure that if Activate is called multiple times it doesn't // overwrite the InitialTransport with a mock transport. if http.DefaultTransport != DefaultTransport { InitialTransport = http.DefaultTransport } http.DefaultTransport = DefaultTransport } // ActivateNonDefault starts the mock environment with a non-default // http.Client. This emulates the Activate function, but allows for // custom clients that do not use http.DefaultTransport // // To enable mocks for a test using a custom client, activate at the // beginning of a test: // client := &http.Client{Transport: &http.Transport{TLSHandshakeTimeout: 60 * time.Second}} // httpmock.ActivateNonDefault(client) func ActivateNonDefault(client *http.Client) { if Disabled() { return } // save the custom client & it's RoundTripper if _, ok := oldClients[client]; !ok { oldClients[client] = client.Transport } client.Transport = DefaultTransport } // GetCallCountInfo gets the info on all the calls httpmock has caught // since it was activated or reset. The info is returned as a map of // the calling keys with the number of calls made to them as their // value. The key is the method, a space, and the url all concatenated // together. // // As a special case, regexp responders generate 2 entries for each // call. One for the call caught and the other for the rule that // matched. For example: // RegisterResponder("GET", `=~z\.com\z`, NewStringResponder(200, "body")) // http.Get("http://z.com") // // will generate the following result: // map[string]int{ // `GET http://z.com`: 1, // `GET =~z\.com\z`: 1, // } func GetCallCountInfo() map[string]int { return DefaultTransport.GetCallCountInfo() } // GetTotalCallCount gets the total number of calls httpmock has taken // since it was activated or reset. func GetTotalCallCount() int { return DefaultTransport.GetTotalCallCount() } // Deactivate shuts down the mock environment. Any HTTP calls made // after this will use a live transport. // // Usually you'll call it in a defer right after activating the mock // environment: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.Deactivate() // // // when this test ends, the mock environment will close // } // // Since go 1.14 you can also use (*testing.T).Cleanup() method as in: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // t.Cleanup(httpmock.Deactivate) // // // when this test ends, the mock environment will close // } // // useful in test helpers to save your callers from calling defer themselves. func Deactivate() { if Disabled() { return } http.DefaultTransport = InitialTransport // reset the custom clients to use their original RoundTripper for oldClient, oldTransport := range oldClients { oldClient.Transport = oldTransport delete(oldClients, oldClient) } } // Reset removes any registered mocks and returns the mock // environment to its initial state. It zeroes call counters too. func Reset() { DefaultTransport.Reset() } // ZeroCallCounters zeroes call counters without touching registered responders. func ZeroCallCounters() { DefaultTransport.ZeroCallCounters() } // DeactivateAndReset is just a convenience method for calling // Deactivate() and then Reset(). // // Happy deferring! func DeactivateAndReset() { Deactivate() Reset() } // RegisterResponder adds a new responder, associated with a given // HTTP method and URL (or path). // // When a request comes in that matches, the responder is called and // the response returned to the client. // // If url contains query parameters, their order matters as well as // their content. All following URLs are here considered as different: // http://z.tld?a=1&b=1 // http://z.tld?b=1&a=1 // http://z.tld?a&b // http://z.tld?a=&b= // // If url begins with "=~", the following chars are considered as a // regular expression. If this regexp can not be compiled, it panics. // Note that the "=~" prefix remains in statistics returned by // GetCallCountInfo(). As 2 regexps can match the same URL, the regexp // responders are tested in the order they are registered. Registering // an already existing regexp responder (same method & same regexp // string) replaces its responder, but does not change its position. // // Registering an already existing responder resets the corresponding // statistics as returned by GetCallCountInfo(). // // Registering a nil Responder removes the existing one and the // corresponding statistics as returned by GetCallCountInfo(). It does // nothing if it does not already exist. // // See RegisterRegexpResponder() to directly pass a *regexp.Regexp. // // Example: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // httpmock.RegisterResponder("GET", "http://example.com/", // httpmock.NewStringResponder(200, "hello world")) // // httpmock.RegisterResponder("GET", "/path/only", // httpmock.NewStringResponder("any host hello world", 200)) // // httpmock.RegisterResponder("GET", `=~^/item/id/\d+\z`, // httpmock.NewStringResponder("any item get", 200)) // // // requests to http://example.com/ now return "hello world" and // // requests to any host with path /path/only return "any host hello world" // // requests to any host with path matching ^/item/id/\d+\z regular expression return "any item get" // } // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting // DefaultTransport.DontCheckMethod to true prior to this call. func RegisterResponder(method, url string, responder Responder) { DefaultTransport.RegisterResponder(method, url, responder) } // RegisterRegexpResponder adds a new responder, associated with a given // HTTP method and URL (or path) regular expression. // // When a request comes in that matches, the responder is called and // the response returned to the client. // // As 2 regexps can match the same URL, the regexp responders are // tested in the order they are registered. Registering an already // existing regexp responder (same method & same regexp string) // replaces its responder, but does not change its position, and // resets the corresponding statistics as returned by GetCallCountInfo(). // // Registering a nil Responder removes the existing one and the // corresponding statistics as returned by GetCallCountInfo(). It does // nothing if it does not already exist. // // A "=~" prefix is added to the stringified regexp in the statistics // returned by GetCallCountInfo(). // // See RegisterResponder function and the "=~" prefix in its url // parameter to avoid compiling the regexp by yourself. // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting // DefaultTransport.DontCheckMethod to true prior to this call. func RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { DefaultTransport.RegisterRegexpResponder(method, urlRegexp, responder) } // RegisterResponderWithQuery it is same as RegisterResponder, but // doesn't depends on query items order. // // query type can be: // url.Values // map[string]string // string, a query string like "a=12&a=13&b=z&c" (see net/url.ParseQuery function) // // If the query type is not recognized or the string cannot be parsed // using net/url.ParseQuery, a panic() occurs. // // Unlike RegisterResponder, path cannot be prefixed by "=~" to say it // is a regexp. If it is, a panic occurs. // // Registering an already existing responder resets the corresponding // statistics as returned by GetCallCountInfo(). // // Registering a nil Responder removes the existing one and the // corresponding statistics as returned by GetCallCountInfo(). It does // nothing if it does not already exist. // // Example using a net/url.Values: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // expectedQuery := net.Values{ // "a": []string{"3", "1", "8"}, // "b": []string{"4", "2"}, // } // httpmock.RegisterResponderWithQueryValues( // "GET", "http://example.com/", expectedQuery, // httpmock.NewStringResponder("hello world", 200)) // // // requests to http://example.com?a=1&a=3&a=8&b=2&b=4 // // and to http://example.com?b=4&a=2&b=2&a=8&a=1 // // now return 'hello world' // } // // or using a map[string]string: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // expectedQuery := map[string]string{ // "a": "1", // "b": "2" // } // httpmock.RegisterResponderWithQuery( // "GET", "http://example.com/", expectedQuery, // httpmock.NewStringResponder("hello world", 200)) // // // requests to http://example.com?a=1&b=2 and http://example.com?b=2&a=1 now return 'hello world' // } // // or using a query string: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // expectedQuery := "a=3&b=4&b=2&a=1&a=8" // httpmock.RegisterResponderWithQueryValues( // "GET", "http://example.com/", expectedQuery, // httpmock.NewStringResponder("hello world", 200)) // // // requests to http://example.com?a=1&a=3&a=8&b=2&b=4 // // and to http://example.com?b=4&a=2&b=2&a=8&a=1 // // now return 'hello world' // } // // If method is a lower-cased version of CONNECT, DELETE, GET, HEAD, // OPTIONS, POST, PUT or TRACE, a panics occurs to notice the possible // mistake. This panic can be disabled by setting // DefaultTransport.DontCheckMethod to true prior to this call. func RegisterResponderWithQuery(method, path string, query interface{}, responder Responder) { DefaultTransport.RegisterResponderWithQuery(method, path, query, responder) } // RegisterNoResponder adds a mock that is called whenever a request // for an unregistered URL is received. The default behavior is to // return a connection error. // // In some cases you may not want all URLs to be mocked, in which case // you can do this: // func TestFetchArticles(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // httpmock.RegisterNoResponder(httpmock.InitialTransport.RoundTrip) // // // any requests that don't have a registered URL will be fetched normally // } func RegisterNoResponder(responder Responder) { DefaultTransport.RegisterNoResponder(responder) } // ErrSubmatchNotFound is the error returned by GetSubmatch* functions // when the given submatch index cannot be found. var ErrSubmatchNotFound = errors.New("submatch not found") // GetSubmatch has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as a // string. Example: // RegisterResponder("GET", `=~^/item/name/([^/]+)\z`, // func(req *http.Request) (*http.Response, error) { // name, err := GetSubmatch(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]interface{}{ // "id": 123, // "name": name, // }) // }) // // It panics if n < 1. See MustGetSubmatch to avoid testing the // returned error. func GetSubmatch(req *http.Request, n int) (string, error) { if n <= 0 { panic(fmt.Sprintf("getting submatches starts at 1, not %d", n)) } n-- submatches := internal.GetSubmatches(req) if n >= len(submatches) { return "", ErrSubmatchNotFound } return submatches[n], nil } // GetSubmatchAsInt has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as an // int64. Example: // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id, err := GetSubmatchAsInt(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]interface{}{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. See MustGetSubmatchAsInt to avoid testing the // returned error. func GetSubmatchAsInt(req *http.Request, n int) (int64, error) { sm, err := GetSubmatch(req, n) if err != nil { return 0, err } return strconv.ParseInt(sm, 10, 64) } // GetSubmatchAsUint has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as a // uint64. Example: // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id, err := GetSubmatchAsUint(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]interface{}{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. See MustGetSubmatchAsUint to avoid testing the // returned error. func GetSubmatchAsUint(req *http.Request, n int) (uint64, error) { sm, err := GetSubmatch(req, n) if err != nil { return 0, err } return strconv.ParseUint(sm, 10, 64) } // GetSubmatchAsFloat has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as a // float64. Example: // RegisterResponder("PATCH", `=~^/item/id/\d+\?height=(\d+(?:\.\d*)?)\z`, // func(req *http.Request) (*http.Response, error) { // height, err := GetSubmatchAsFloat(req, 1) // 1=first regexp submatch // if err != nil { // return nil, err // } // return NewJsonResponse(200, map[string]interface{}{ // "id": id, // "name": "The beautiful name", // "height": height, // }) // }) // // It panics if n < 1. See MustGetSubmatchAsFloat to avoid testing the // returned error. func GetSubmatchAsFloat(req *http.Request, n int) (float64, error) { sm, err := GetSubmatch(req, n) if err != nil { return 0, err } return strconv.ParseFloat(sm, 64) } // MustGetSubmatch works as GetSubmatch except that it panics in case // of error (submatch not found). It has to be used in Responders // installed by RegisterRegexpResponder or RegisterResponder + "=~" // URL prefix. It allows to retrieve the n-th submatch of the matching // regexp, as a string. Example: // RegisterResponder("GET", `=~^/item/name/([^/]+)\z`, // func(req *http.Request) (*http.Response, error) { // name := MustGetSubmatch(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]interface{}{ // "id": 123, // "name": name, // }) // }) // // It panics if n < 1. func MustGetSubmatch(req *http.Request, n int) string { s, err := GetSubmatch(req, n) if err != nil { panic("GetSubmatch failed: " + err.Error()) } return s } // MustGetSubmatchAsInt works as GetSubmatchAsInt except that it // panics in case of error (submatch not found or invalid int64 // format). It has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as an // int64. Example: // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id := MustGetSubmatchAsInt(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]interface{}{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. func MustGetSubmatchAsInt(req *http.Request, n int) int64 { i, err := GetSubmatchAsInt(req, n) if err != nil { panic("GetSubmatchAsInt failed: " + err.Error()) } return i } // MustGetSubmatchAsUint works as GetSubmatchAsUint except that it // panics in case of error (submatch not found or invalid uint64 // format). It has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as a // uint64. Example: // RegisterResponder("GET", `=~^/item/id/(\d+)\z`, // func(req *http.Request) (*http.Response, error) { // id, err := MustGetSubmatchAsUint(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]interface{}{ // "id": id, // "name": "The beautiful name", // }) // }) // // It panics if n < 1. func MustGetSubmatchAsUint(req *http.Request, n int) uint64 { u, err := GetSubmatchAsUint(req, n) if err != nil { panic("GetSubmatchAsUint failed: " + err.Error()) } return u } // MustGetSubmatchAsFloat works as GetSubmatchAsFloat except that it // panics in case of error (submatch not found or invalid float64 // format). It has to be used in Responders installed by // RegisterRegexpResponder or RegisterResponder + "=~" URL prefix. It // allows to retrieve the n-th submatch of the matching regexp, as a // float64. Example: // RegisterResponder("PATCH", `=~^/item/id/\d+\?height=(\d+(?:\.\d*)?)\z`, // func(req *http.Request) (*http.Response, error) { // height := MustGetSubmatchAsFloat(req, 1) // 1=first regexp submatch // return NewJsonResponse(200, map[string]interface{}{ // "id": id, // "name": "The beautiful name", // "height": height, // }) // }) // // It panics if n < 1. func MustGetSubmatchAsFloat(req *http.Request, n int) float64 { f, err := GetSubmatchAsFloat(req, n) if err != nil { panic("GetSubmatchAsFloat failed: " + err.Error()) } return f } httpmock-1.1.0/transport_test.go000066400000000000000000000721101416056172200167710ustar00rootroot00000000000000package httpmock_test import ( "bytes" "context" "encoding/json" "errors" "net" "net/http" "net/url" "reflect" "regexp" "strings" "testing" "time" . "github.com/jarcoal/httpmock" "github.com/jarcoal/httpmock/internal" ) const testURL = "http://www.example.com/" func TestMockTransport(t *testing.T) { Activate() defer Deactivate() url := "https://github.com/" body := `["hello world"]` + "\n" RegisterResponder("GET", url, NewStringResponder(200, body)) RegisterResponder("GET", `=~/xxx\z`, NewStringResponder(200, body)) // Read it as a simple string (ioutil.ReadAll of assertBody will // trigger io.EOF) func() { resp, err := http.Get(url) if err != nil { t.Fatal(err) } if !assertBody(t, resp, body) { t.FailNow() } // the http client wraps our NoResponderFound error, so we just try and match on text _, err = http.Get(testURL) if err == nil { t.Fatal("An error should occur") } if !strings.HasSuffix(err.Error(), NoResponderFound.Error()) { t.Fatal(err) } // Use wrongly cased method, the error should warn us req, err := http.NewRequest("Get", url, nil) if err != nil { t.Fatal(err) } c := http.Client{} _, err = c.Do(req) if err == nil { t.Fatal("An error should occur") } if !strings.HasSuffix(err.Error(), NoResponderFound.Error()+" for method Get, but one matches method GET") { t.Fatal(err) } // Use POST instead of GET, the error should warn us req, err = http.NewRequest("POST", url, nil) if err != nil { t.Fatal(err) } _, err = c.Do(req) if err == nil { t.Fatal("An error should occur") } if !strings.HasSuffix(err.Error(), NoResponderFound.Error()+" for method POST, but one matches method GET") { t.Fatal(err) } // Same using a regexp responder req, err = http.NewRequest("POST", "http://pipo.com/xxx", nil) if err != nil { t.Fatal(err) } _, err = c.Do(req) if err == nil { t.Fatal("An error should occur") } if !strings.HasSuffix(err.Error(), NoResponderFound.Error()+" for method POST, but one matches method GET") { t.Fatal(err) } }() // Do it again, but twice with json decoder (json Decode will not // reach EOF, but Close is called as the JSON response is complete) for i := 0; i < 2; i++ { func() { resp, err := http.Get(url) if err != nil { t.Fatal(err) } defer resp.Body.Close() var res []string err = json.NewDecoder(resp.Body).Decode(&res) if err != nil { t.Fatal(err) } if len(res) != 1 || res[0] != "hello world" { t.Fatalf(`%v read instead of ["hello world"]`, res) } }() } } // We should be able to find GET handlers when using an http.Request with a // default (zero-value) .Method. func TestMockTransportDefaultMethod(t *testing.T) { Activate() defer Deactivate() const urlString = "https://github.com/" url, err := url.Parse(urlString) if err != nil { t.Fatal(err) } body := "hello world" RegisterResponder("GET", urlString, NewStringResponder(200, body)) req := &http.Request{ URL: url, // Note: Method unspecified (zero-value) } client := &http.Client{} resp, err := client.Do(req) if err != nil { t.Fatal(err) } assertBody(t, resp, body) } func TestMockTransportReset(t *testing.T) { DeactivateAndReset() if DefaultTransport.NumResponders() > 0 { t.Fatal("expected no responders at this point") } RegisterResponder("GET", testURL, NewStringResponder(200, "hey")) if DefaultTransport.NumResponders() != 1 { t.Fatal("expected one responder") } Reset() if DefaultTransport.NumResponders() > 0 { t.Fatal("expected no responders as they were just reset") } } func TestMockTransportNoResponder(t *testing.T) { Activate() defer DeactivateAndReset() Reset() if _, err := http.Get(testURL); err == nil { t.Fatal("expected to receive a connection error due to lack of responders") } RegisterNoResponder(NewStringResponder(200, "hello world")) resp, err := http.Get(testURL) if err != nil { t.Fatal("expected request to succeed") } assertBody(t, resp, "hello world") // Using NewNotFoundResponder() RegisterNoResponder(NewNotFoundResponder(nil)) _, err = http.Get(testURL) if err == nil { t.Fatal("an error should occur") } if !strings.HasSuffix(err.Error(), "Responder not found for GET http://www.example.com/") { t.Fatalf("Unexpected error content: %s", err) } // Help the user in case a Responder exists for another method RegisterResponder("POST", testURL, NewStringResponder(200, "hello world")) _, err = http.Get(testURL) if err == nil { t.Fatal("an error should occur") } if !strings.HasSuffix(err.Error(), "Responder not found for GET http://www.example.com/, but one matches method POST") { t.Fatalf("Unexpected error content: %s", err) } } func TestMockTransportQuerystringFallback(t *testing.T) { Activate() defer DeactivateAndReset() // register the testURL responder RegisterResponder("GET", testURL, NewStringResponder(200, "hello world")) for _, suffix := range []string{"?", "?hello=world", "?hello=world#foo", "?hello=world&hello=all", "#foo"} { reqURL := testURL + suffix t.Log(reqURL) // make a request for the testURL with a querystring resp, err := http.Get(reqURL) if err != nil { t.Fatalf("expected request %s to succeed", reqURL) } assertBody(t, resp, "hello world") } } func TestMockTransportPathOnlyFallback(t *testing.T) { // Just in case a panic occurs defer DeactivateAndReset() for _, test := range []struct { Responder string Paths []string }{ { // unsorted query string matches exactly Responder: "/hello/world?query=string&abc=zz#fragment", Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", }, }, { // sorted query string matches all cases Responder: "/hello/world?abc=zz&query=string#fragment", Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?abc=zz&query=string#fragment", }, }, { // unsorted query string matches exactly Responder: "/hello/world?query=string&abc=zz", Paths: []string{ testURL + "hello/world?query=string&abc=zz", }, }, { // sorted query string matches all cases Responder: "/hello/world?abc=zz&query=string", Paths: []string{ testURL + "hello/world?query=string&abc=zz", testURL + "hello/world?abc=zz&query=string", }, }, { // unsorted query string matches exactly Responder: "/hello/world?query=string&query=string2&abc=zz", Paths: []string{ testURL + "hello/world?query=string&query=string2&abc=zz", }, }, // sorted query string matches all cases { Responder: "/hello/world?abc=zz&query=string&query=string2", Paths: []string{ testURL + "hello/world?query=string&query=string2&abc=zz", testURL + "hello/world?query=string2&query=string&abc=zz", testURL + "hello/world?abc=zz&query=string2&query=string", }, }, { Responder: "/hello/world?query", Paths: []string{ testURL + "hello/world?query", }, }, { Responder: "/hello/world?query&abc", Paths: []string{ testURL + "hello/world?query&abc", // testURL + "hello/world?abc&query" won' work as "=" is needed, see below }, }, { // In case the sorting does not matter for received params without // values, we must register params with "=" Responder: "/hello/world?abc=&query=", Paths: []string{ testURL + "hello/world?query&abc", testURL + "hello/world?abc&query", }, }, { Responder: "/hello/world#fragment", Paths: []string{ testURL + "hello/world#fragment", }, }, { Responder: "/hello/world", Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?query=string&abc=zz", testURL + "hello/world#fragment", testURL + "hello/world", }, }, // Regexp cases { Responder: `=~^http://.*/hello/.*ld\z`, Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?query=string&abc=zz", testURL + "hello/world#fragment", testURL + "hello/world", }, }, { Responder: `=~^http://.*/hello/.*ld(\z|[?#])`, Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?query=string&abc=zz", testURL + "hello/world#fragment", testURL + "hello/world", }, }, { Responder: `=~^/hello/.*ld\z`, Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?query=string&abc=zz", testURL + "hello/world#fragment", testURL + "hello/world", }, }, { Responder: `=~^/hello/.*ld(\z|[?#])`, Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?query=string&abc=zz", testURL + "hello/world#fragment", testURL + "hello/world", }, }, { Responder: `=~abc=zz`, Paths: []string{ testURL + "hello/world?query=string&abc=zz#fragment", testURL + "hello/world?query=string&abc=zz", }, }, } { Activate() // register the responder RegisterResponder("GET", test.Responder, NewStringResponder(200, "hello world")) for _, reqURL := range test.Paths { t.Logf("%s: %s", test.Responder, reqURL) // make a request for the testURL with a querystring resp, err := http.Get(reqURL) if err != nil { t.Errorf("%s: expected request %s to succeed", test.Responder, reqURL) continue } assertBody(t, resp, "hello world") } DeactivateAndReset() } } type dummyTripper struct{} func (d *dummyTripper) RoundTrip(*http.Request) (*http.Response, error) { return nil, nil } func TestMockTransportInitialTransport(t *testing.T) { DeactivateAndReset() tripper := &dummyTripper{} http.DefaultTransport = tripper Activate() if http.DefaultTransport == tripper { t.Fatal("expected http.DefaultTransport to be a mock transport") } Deactivate() if http.DefaultTransport != tripper { t.Fatal("expected http.DefaultTransport to be dummy") } } func TestMockTransportNonDefault(t *testing.T) { // create a custom http client w/ custom Roundtripper client := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 60 * time.Second, KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 60 * time.Second, }, } // activate mocks for the client ActivateNonDefault(client) defer DeactivateAndReset() body := "hello world!" RegisterResponder("GET", testURL, NewStringResponder(200, body)) req, err := http.NewRequest("GET", testURL, nil) if err != nil { t.Fatal(err) } resp, err := client.Do(req) if err != nil { t.Fatal(err) } assertBody(t, resp, body) } func TestMockTransportRespectsCancel(t *testing.T) { Activate() defer DeactivateAndReset() const ( cancelNone = iota cancelReq cancelCtx ) cases := []struct { withCancel int cancelNow bool withPanic bool expectedBody string expectedErr error }{ // No cancel specified at all. Falls back to normal behavior {cancelNone, false, false, "hello world", nil}, // Cancel returns error {cancelReq, true, false, "", errors.New("request canceled")}, // Cancel via context returns error {cancelCtx, true, false, "", errors.New("context canceled")}, // Request can be cancelled but it is not cancelled. {cancelReq, false, false, "hello world", nil}, // Request can be cancelled but it is not cancelled. {cancelCtx, false, false, "hello world", nil}, // Panic in cancelled request is handled {cancelReq, false, true, "", errors.New(`panic in responder: got "oh no"`)}, // Panic in cancelled request is handled {cancelCtx, false, true, "", errors.New(`panic in responder: got "oh no"`)}, } for _, c := range cases { Reset() if c.withPanic { RegisterResponder("GET", testURL, func(r *http.Request) (*http.Response, error) { time.Sleep(10 * time.Millisecond) panic("oh no") }) } else { RegisterResponder("GET", testURL, func(r *http.Request) (*http.Response, error) { time.Sleep(10 * time.Millisecond) return NewStringResponse(http.StatusOK, "hello world"), nil }) } req, err := http.NewRequest("GET", testURL, nil) if err != nil { t.Fatal(err) } switch c.withCancel { case cancelReq: cancel := make(chan struct{}, 1) req.Cancel = cancel // nolint: staticcheck if c.cancelNow { cancel <- struct{}{} } case cancelCtx: ctx, cancel := context.WithCancel(req.Context()) req = req.WithContext(ctx) if c.cancelNow { cancel() } else { defer cancel() // avoid ctx leak } } resp, err := http.DefaultClient.Do(req) // If we expect an error but none was returned, it's fatal for this test... if err == nil && c.expectedErr != nil { t.Fatal("Error should not be nil") } if err != nil { got := err.(*url.Error) // Do not use reflect.DeepEqual as go 1.13 includes stack frames // into errors issued by errors.New() if c.expectedErr == nil || got.Err.Error() != c.expectedErr.Error() { t.Errorf("Expected error: %v, got: %v", c.expectedErr, got.Err) } } if c.expectedBody != "" { assertBody(t, resp, c.expectedBody) } } } func TestMockTransportRespectsTimeout(t *testing.T) { timeout := time.Millisecond client := &http.Client{ Timeout: timeout, } ActivateNonDefault(client) defer DeactivateAndReset() RegisterResponder( "GET", testURL, func(r *http.Request) (*http.Response, error) { time.Sleep(100 * timeout) return NewStringResponse(http.StatusOK, ""), nil }, ) _, err := client.Get(testURL) if err == nil { t.Fail() } } func TestMockTransportCallCountReset(t *testing.T) { Reset() Activate() defer Deactivate() const ( url = "https://github.com/path?b=1&a=2" url2 = "https://gitlab.com/" ) RegisterResponder("GET", url, NewStringResponder(200, "body")) RegisterResponder("POST", "=~gitlab", NewStringResponder(200, "body")) _, err := http.Get(url) if err != nil { t.Fatal(err) } buff := new(bytes.Buffer) json.NewEncoder(buff).Encode("{}") // nolint: errcheck _, err = http.Post(url2, "application/json", buff) if err != nil { t.Fatal(err) } _, err = http.Get(url) if err != nil { t.Fatal(err) } totalCallCount := GetTotalCallCount() if totalCallCount != 3 { t.Fatalf("did not track the total count of calls correctly. expected it to be 3, but it was %v", totalCallCount) } info := GetCallCountInfo() expectedInfo := map[string]int{ "GET " + url: 2, // Regexp match generates 2 entries: "POST " + url2: 1, // the matched call "POST =~gitlab": 1, // the regexp responder } if !reflect.DeepEqual(info, expectedInfo) { t.Fatalf("did not correctly track the call count info. expected it to be \n %+v\n but it was \n %+v", expectedInfo, info) } Reset() afterResetTotalCallCount := GetTotalCallCount() if afterResetTotalCallCount != 0 { t.Fatalf("did not reset the total count of calls correctly. expected it to be 0 after reset, but it was %v", afterResetTotalCallCount) } info = GetCallCountInfo() if !reflect.DeepEqual(info, map[string]int{}) { t.Fatalf("did not correctly reset the call count info. expected it to be \n {}\n but it was \n %+v", info) } } func TestMockTransportCallCountZero(t *testing.T) { Reset() Activate() defer Deactivate() const ( url = "https://github.com/path?b=1&a=2" url2 = "https://gitlab.com/" ) RegisterResponder("GET", url, NewStringResponder(200, "body")) RegisterResponder("POST", "=~gitlab", NewStringResponder(200, "body")) _, err := http.Get(url) if err != nil { t.Fatal(err) } buff := new(bytes.Buffer) json.NewEncoder(buff).Encode("{}") // nolint: errcheck _, err = http.Post(url2, "application/json", buff) if err != nil { t.Fatal(err) } _, err = http.Get(url) if err != nil { t.Fatal(err) } totalCallCount := GetTotalCallCount() if totalCallCount != 3 { t.Fatalf("did not track the total count of calls correctly. expected it to be 3, but it was %v", totalCallCount) } info := GetCallCountInfo() expectedInfo := map[string]int{ "GET " + url: 2, // Regexp match generates 2 entries: "POST " + url2: 1, // the matched call "POST =~gitlab": 1, // the regexp responder } if !reflect.DeepEqual(info, expectedInfo) { t.Fatalf("did not correctly track the call count info. expected it to be \n %+v\n but it was \n %+v", expectedInfo, info) } ZeroCallCounters() afterResetTotalCallCount := GetTotalCallCount() if afterResetTotalCallCount != 0 { t.Fatalf("did not reset the total count of calls correctly. expected it to be 0 after reset, but it was %v", afterResetTotalCallCount) } info = GetCallCountInfo() expectedInfo = map[string]int{ "GET " + url: 0, // Regexp match generates 2 entries: "POST " + url2: 0, // the matched call "POST =~gitlab": 0, // the regexp responder } if !reflect.DeepEqual(info, expectedInfo) { t.Fatalf("did not correctly reset the call count info. expected it to be \n %+v\n but it was \n %+v", expectedInfo, info) } // Unregister each responder RegisterResponder("GET", url, nil) RegisterResponder("POST", "=~gitlab", nil) info = GetCallCountInfo() expectedInfo = map[string]int{ // this one remains as it is not directly related to a registered // responder but a consequence of a regexp match "POST " + url2: 0, } if !reflect.DeepEqual(info, expectedInfo) { t.Fatalf("did not correctly reset the call count info. expected it to be \n %+v\n but it was \n %+v", expectedInfo, info) } } func TestRegisterResponderWithQuery(t *testing.T) { Reset() // Just in case a panic occurs defer DeactivateAndReset() // create a custom http client w/ custom Roundtripper client := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 60 * time.Second, KeepAlive: 30 * time.Second, }).Dial, TLSHandshakeTimeout: 60 * time.Second, }, } body := "hello world!" testURLPath := "http://acme.test/api" for _, test := range []struct { URL string Queries []interface{} URLs []string }{ { Queries: []interface{}{ map[string]string{"a": "1", "b": "2"}, "a=1&b=2", "b=2&a=1", url.Values{"a": []string{"1"}, "b": []string{"2"}}, }, URLs: []string{ "http://acme.test/api?a=1&b=2", "http://acme.test/api?b=2&a=1", }, }, { Queries: []interface{}{ url.Values{ "a": []string{"3", "2", "1"}, "b": []string{"4", "2"}, "c": []string{""}, // is the net/url way to record params without values // Test: // u, _ := url.Parse("/hello/world?query") // fmt.Printf("%d<%s>\n", len(u.Query()["query"]), u.Query()["query"][0]) // // prints "1<>" }, "a=1&b=2&a=3&c&b=4&a=2", "b=2&a=1&c=&b=4&a=2&a=3", nil, }, URLs: []string{ testURLPath + "?a=1&b=2&a=3&c&b=4&a=2", testURLPath + "?a=1&b=2&a=3&c=&b=4&a=2", testURLPath + "?b=2&a=1&c=&b=4&a=2&a=3", testURLPath + "?b=2&a=1&c&b=4&a=2&a=3", }, }, } { for _, query := range test.Queries { ActivateNonDefault(client) RegisterResponderWithQuery("GET", testURLPath, query, NewStringResponder(200, body)) for _, url := range test.URLs { t.Logf("query=%v URL=%s", query, url) req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatal(err) } resp, err := client.Do(req) if err != nil { t.Fatal(err) } assertBody(t, resp, body) } if info := GetCallCountInfo(); len(info) != 1 { t.Fatalf("%s: len(GetCallCountInfo()) should be 1 but contains %+v", testURLPath, info) } // Remove... RegisterResponderWithQuery("GET", testURLPath, query, nil) if info := GetCallCountInfo(); len(info) != 0 { t.Fatalf("did not correctly reset the call count info, it still contains %+v", info) } for _, url := range test.URLs { t.Logf("query=%v URL=%s", query, url) req, err := http.NewRequest("GET", url, nil) if err != nil { t.Fatal(err) } _, err = client.Do(req) if err == nil { t.Fatalf("No error occurred for %s", url) } if !strings.HasSuffix(err.Error(), "no responder found") { t.Errorf("Not expected error suffix: %s", err) } } DeactivateAndReset() } } } func TestRegisterResponderWithQueryPanic(t *testing.T) { resp := NewStringResponder(200, "hello world!") for _, test := range []struct { Path string Query interface{} PanicPrefix string }{ { Path: "foobar", Query: "%", PanicPrefix: "RegisterResponderWithQuery bad query string: ", }, { Path: "foobar", Query: 1234, PanicPrefix: "RegisterResponderWithQuery bad query type int. Only url.Values, map[string]string and string are allowed", }, { Path: `=~regexp.*\z`, Query: "", PanicPrefix: `path begins with "=~", RegisterResponder should be used instead of RegisterResponderWithQuery`, }, } { panicked, panicStr := catchPanic(func() { RegisterResponderWithQuery("GET", test.Path, test.Query, resp) }) if !panicked { t.Errorf("RegisterResponderWithQuery + query=%v did not panic", test.Query) continue } if !strings.HasPrefix(panicStr, test.PanicPrefix) { t.Fatalf(`RegisterResponderWithQuery + query=%v panic="%v" expected prefix="%v"`, test.Query, panicStr, test.PanicPrefix) } } } func TestRegisterRegexpResponder(t *testing.T) { Activate() defer DeactivateAndReset() rx := regexp.MustCompile("ex.mple") RegisterRegexpResponder("GET", rx, NewStringResponder(200, "first")) // Overwrite responder RegisterRegexpResponder("GET", rx, NewStringResponder(200, "second")) resp, err := http.Get(testURL) if err != nil { t.Fatalf("expected request %s to succeed", testURL) } assertBody(t, resp, "second") } func TestSubmatches(t *testing.T) { req, err := http.NewRequest("GET", "/foo/bar", nil) if err != nil { t.Fatal(err) } req2 := internal.SetSubmatches(req, []string{"foo", "123", "-123", "12.3"}) t.Run("GetSubmatch", func(t *testing.T) { _, err := GetSubmatch(req, 1) if err != ErrSubmatchNotFound { t.Errorf("Submatch should not be found in req: %v", err) } _, err = GetSubmatch(req2, 5) if err != ErrSubmatchNotFound { t.Errorf("Submatch #5 should not be found in req2: %v", err) } s, err := GetSubmatch(req2, 1) if err != nil { t.Errorf("GetSubmatch(req2, 1) failed: %v", err) } if s != "foo" { t.Errorf("GetSubmatch(req2, 1) failed, got: %v, expected: foo", s) } s, err = GetSubmatch(req2, 4) if err != nil { t.Errorf("GetSubmatch(req2, 4) failed: %v", err) } if s != "12.3" { t.Errorf("GetSubmatch(req2, 4) failed, got: %v, expected: 12.3", s) } s = MustGetSubmatch(req2, 4) if s != "12.3" { t.Errorf("GetSubmatch(req2, 4) failed, got: %v, expected: 12.3", s) } }) t.Run("GetSubmatchAsInt", func(t *testing.T) { _, err := GetSubmatchAsInt(req, 1) if err != ErrSubmatchNotFound { t.Errorf("Submatch should not be found in req: %v", err) } _, err = GetSubmatchAsInt(req2, 4) // not an int if err == nil || err == ErrSubmatchNotFound { t.Errorf("Submatch should not be an int64: %v", err) } i, err := GetSubmatchAsInt(req2, 3) if err != nil { t.Errorf("GetSubmatchAsInt(req2, 3) failed: %v", err) } if i != -123 { t.Errorf("GetSubmatchAsInt(req2, 3) failed, got: %d, expected: -123", i) } i = MustGetSubmatchAsInt(req2, 3) if i != -123 { t.Errorf("MustGetSubmatchAsInt(req2, 3) failed, got: %d, expected: -123", i) } }) t.Run("GetSubmatchAsUint", func(t *testing.T) { _, err := GetSubmatchAsUint(req, 1) if err != ErrSubmatchNotFound { t.Errorf("Submatch should not be found in req: %v", err) } _, err = GetSubmatchAsUint(req2, 3) // not a uint if err == nil || err == ErrSubmatchNotFound { t.Errorf("Submatch should not be an uint64: %v", err) } u, err := GetSubmatchAsUint(req2, 2) if err != nil { t.Errorf("GetSubmatchAsUint(req2, 2) failed: %v", err) } if u != 123 { t.Errorf("GetSubmatchAsUint(req2, 2) failed, got: %d, expected: 123", u) } u = MustGetSubmatchAsUint(req2, 2) if u != 123 { t.Errorf("MustGetSubmatchAsUint(req2, 2) failed, got: %d, expected: 123", u) } }) t.Run("GetSubmatchAsFloat", func(t *testing.T) { _, err := GetSubmatchAsFloat(req, 1) if err != ErrSubmatchNotFound { t.Errorf("Submatch should not be found in req: %v", err) } _, err = GetSubmatchAsFloat(req2, 1) // not a float if err == nil || err == ErrSubmatchNotFound { t.Errorf("Submatch should not be an float64: %v", err) } f, err := GetSubmatchAsFloat(req2, 4) if err != nil { t.Errorf("GetSubmatchAsFloat(req2, 4) failed: %v", err) } if f != 12.3 { t.Errorf("GetSubmatchAsFloat(req2, 4) failed, got: %f, expected: 12.3", f) } f = MustGetSubmatchAsFloat(req2, 4) if f != 12.3 { t.Errorf("MustGetSubmatchAsFloat(req2, 4) failed, got: %f, expected: 12.3", f) } }) t.Run("GetSubmatch* panics", func(t *testing.T) { for _, test := range []struct { Name string Fn func() PanicPrefix string }{ { Name: "GetSubmatch & n < 1", Fn: func() { GetSubmatch(req, 0) }, // nolint: errcheck PanicPrefix: "getting submatches starts at 1, not 0", }, { Name: "MustGetSubmatch", Fn: func() { MustGetSubmatch(req, 1) }, PanicPrefix: "GetSubmatch failed: " + ErrSubmatchNotFound.Error(), }, { Name: "MustGetSubmatchAsInt", Fn: func() { MustGetSubmatchAsInt(req2, 4) }, // not an int PanicPrefix: "GetSubmatchAsInt failed: ", }, { Name: "MustGetSubmatchAsUint", Fn: func() { MustGetSubmatchAsUint(req2, 3) }, // not a uint PanicPrefix: "GetSubmatchAsUint failed: ", }, { Name: "GetSubmatchAsFloat", Fn: func() { MustGetSubmatchAsFloat(req2, 1) }, // not a float PanicPrefix: "GetSubmatchAsFloat failed: ", }, } { var ( didntPanic bool panicVal interface{} ) func() { defer func() { panicVal = recover() }() test.Fn() didntPanic = true }() if didntPanic { t.Errorf("%s did not panic", test.Name) } panicStr, ok := panicVal.(string) if !ok || !strings.HasPrefix(panicStr, test.PanicPrefix) { t.Errorf(`%s panic="%v" expected prefix="%v"`, test.Name, panicVal, test.PanicPrefix) } } }) t.Run("Full test", func(t *testing.T) { Activate() defer DeactivateAndReset() var ( id uint64 delta float64 deltaStr string inc int64 ) RegisterResponder("GET", `=~^/id/(\d+)\?delta=(\d+(?:\.\d*)?)&inc=(-?\d+)\z`, func(req *http.Request) (*http.Response, error) { id = MustGetSubmatchAsUint(req, 1) delta = MustGetSubmatchAsFloat(req, 2) deltaStr = MustGetSubmatch(req, 2) inc = MustGetSubmatchAsInt(req, 3) return NewStringResponse(http.StatusOK, "OK"), nil }) resp, err := http.Get("http://example.tld/id/123?delta=1.2&inc=-5") if err != nil { t.Fatal(err) } assertBody(t, resp, "OK") // Check submatches if id != 123 { t.Errorf("seems MustGetSubmatchAsUint failed, got: %d, expected: 123", id) } if delta != 1.2 { t.Errorf("seems MustGetSubmatchAsFloat failed, got: %f, expected: 1.2", delta) } if deltaStr != "1.2" { t.Errorf("seems MustGetSubmatch failed, got: %v, expected: 1.2", deltaStr) } if inc != -5 { t.Errorf("seems MustGetSubmatchAsInt failed, got: %d, expected: 123", inc) } }) } func TestCheckStackTracer(t *testing.T) { // Full test using Trace() Responder Activate() defer Deactivate() const url = "https://foo.bar/" var mesg string RegisterResponder("GET", url, NewStringResponder(200, "{}"). Trace(func(args ...interface{}) { mesg = args[0].(string) })) resp, err := http.Get(url) if err != nil { t.Fatal(err) } if !assertBody(t, resp, "{}") { t.FailNow() } // Check that first frame is the net/http.Get() call if !strings.HasPrefix(mesg, "GET https://foo.bar/\nCalled from net/http.Get()\n at ") || strings.HasSuffix(mesg, "\n") { t.Errorf("Bad mesg: <%v>", mesg) } } func TestCheckMethod(t *testing.T) { mt := NewMockTransport() var ( panicked bool panicStr string ) // // Panics checkPanic := func() { helper(t).Helper() if panicStr != `You probably want to use method "GET" instead of "get"? If not and so want to disable this check, set MockTransport.DontCheckMethod field to true` { if panicked { t.Errorf("Wrong panic mesg: %s", panicStr) } else { t.Error("Did not panic!") } } } panicked, panicStr = catchPanic(func() { mt.RegisterResponder("get", "/pipo", NewStringResponder(200, "")) }) checkPanic() panicked, panicStr = catchPanic(func() { mt.RegisterRegexpResponder("get", regexp.MustCompile("."), NewStringResponder(200, "")) }) checkPanic() panicked, panicStr = catchPanic(func() { mt.RegisterResponderWithQuery("get", "/pipo", url.Values(nil), NewStringResponder(200, "")) }) checkPanic() // // No longer panics checkNoPanic := func() { helper(t).Helper() if panicked { t.Errorf("Should not panic! but %s", panicStr) } } mt.DontCheckMethod = true panicked, panicStr = catchPanic(func() { mt.RegisterResponder("get", "/pipo", NewStringResponder(200, "")) }) checkNoPanic() panicked, panicStr = catchPanic(func() { mt.RegisterRegexpResponder("get", regexp.MustCompile("."), NewStringResponder(200, "")) }) checkNoPanic() panicked, panicStr = catchPanic(func() { mt.RegisterResponderWithQuery("get", "/pipo", url.Values(nil), NewStringResponder(200, "")) }) checkNoPanic() } httpmock-1.1.0/util_test.go000066400000000000000000000025171416056172200157160ustar00rootroot00000000000000package httpmock_test import ( "fmt" "io/ioutil" "net/http" "os" "testing" ) func assertBody(t *testing.T, resp *http.Response, expected string) bool { defer resp.Body.Close() helper(t).Helper() data, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatal(err) } got := string(data) if got != expected { t.Errorf("Got body: %#v, expected: %#v", got, expected) return false } return true } // Stolen from https://github.com/maxatome/go-testdeep func catchPanic(fn func()) (panicked bool, ret string) { func() { defer func() { panicParam := recover() if panicked { ret = fmt.Sprint(panicParam) } }() panicked = true fn() panicked = false }() return } func tmpDir(t *testing.T) (string, func()) { dir, err := ioutil.TempDir("", "httpmock") if err != nil { helper(t).Helper() t.Fatal(err) } return dir, func() { os.RemoveAll(dir) } } func writeFile(t *testing.T, file string, content []byte) { err := ioutil.WriteFile(file, content, 0644) if err != nil { helper(t).Helper() t.Fatal(err) } } // fakeHelper allows to compensate the absence of // (*testing.T).Helper() in go<1.9. type fakeHelper struct{} func (f fakeHelper) Helper() {} type helperAble interface { Helper() } func helper(t interface{}) helperAble { if th, ok := t.(helperAble); ok { return th } return fakeHelper{} }