pax_global_header00006660000000000000000000000064144671440500014517gustar00rootroot0000000000000052 comment=8b32cd6f52e32195f15b55c5217c12c29d6004b5 httpmock-1.3.1/000077500000000000000000000000001446714405000133525ustar00rootroot00000000000000httpmock-1.3.1/.github/000077500000000000000000000000001446714405000147125ustar00rootroot00000000000000httpmock-1.3.1/.github/workflows/000077500000000000000000000000001446714405000167475ustar00rootroot00000000000000httpmock-1.3.1/.github/workflows/ci.yml000066400000000000000000000047051446714405000200730ustar00rootroot00000000000000name: Build on: push: branches: [ v1 ] pull_request: branches: [ v1 ] workflow_dispatch: jobs: test: strategy: matrix: go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x, tip] full-tests: [false] include: - go-version: 1.21.x full-tests: true runs-on: ubuntu-latest steps: - name: Setup go run: | curl -sL https://raw.githubusercontent.com/maxatome/install-go/v3.5/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.54.1 $HOME/go/bin/golangci-lint run --max-issues-per-linter 0 \ --max-same-issues 0 \ --exclude="unused-parameter: parameter '[^']+' seems to be unused, consider removing or renaming it as _" \ -E asciicheck \ -E bidichk \ -E durationcheck \ -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 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 install github.com/mattn/goveralls@v0.0.11 goveralls -coverprofile=coverage.out -service=github httpmock-1.3.1/.gitignore000066400000000000000000000003741446714405000153460ustar00rootroot00000000000000# 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.3.1/LICENSE000066400000000000000000000020661446714405000143630ustar00rootroot00000000000000The 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.3.1/README.md000066400000000000000000000171541446714405000146410ustar00rootroot00000000000000# 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.13 to 1.21 and is regularly tested against tip. `v1` branch has to be used instead of `master`. 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 latest httpmock release, now [![Version](https://img.shields.io/github/tag/jarcoal/httpmock.svg)](https://github.com/jarcoal/httpmock/releases). ## 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 }) // mock to add a specific article, send a Bad Request response // when the request body contains `"type":"toy"` httpmock.RegisterMatcherResponder("POST", "https://api.mybiz.com/articles", httpmock.BodyContainsString(`"type":"toy"`), httpmock.NewStringResponder(400, `{"reason":"Invalid article type"}`)) // 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. ### [go-testdeep](https://go-testdeep.zetta.rocks/) + [tdsuite](https://pkg.go.dev/github.com/maxatome/go-testdeep/helpers/tdsuite) example: ```go // article_test.go import ( "testing" "github.com/jarcoal/httpmock" "github.com/maxatome/go-testdeep/helpers/tdsuite" "github.com/maxatome/go-testdeep/td" ) type MySuite struct{} func (s *MySuite) Setup(t *td.T) error { // block all HTTP requests httpmock.Activate() return nil } func (s *MySuite) PostTest(t *td.T, testName string) error { // remove any mocks after each test httpmock.Reset() return nil } func (s *MySuite) Destroy(t *td.T) error { httpmock.DeactivateAndReset() return nil } func TestMySuite(t *testing.T) { tdsuite.Run(t, &MySuite{}) } func (s *MySuite) TestArticles(assert, require *td.T) { 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/) 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.3.1/any.go000066400000000000000000000001171446714405000144670ustar00rootroot00000000000000//go:build !go1.18 // +build !go1.18 package httpmock type any = interface{} httpmock-1.3.1/doc.go000066400000000000000000000054401446714405000144510ustar00rootroot00000000000000/* 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]any, 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]any{ "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]any) 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.3.1/env.go000066400000000000000000000003571446714405000144760ustar00rootroot00000000000000package 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.3.1/env_test.go000066400000000000000000000033621446714405000155340ustar00rootroot00000000000000package httpmock_test import ( "net/http" "os" "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock" ) const envVarName = "GONOMOCKS" func TestEnv(t *testing.T) { require := td.Require(t) httpmock.DeactivateAndReset() defer func(orig string) { require.CmpNoError(os.Setenv(envVarName, orig)) }(os.Getenv(envVarName)) // put it in an enabled state require.CmpNoError(os.Setenv(envVarName, "")) require.False(httpmock.Disabled(), "expected not to be disabled") client1 := &http.Client{Transport: &http.Transport{}} client2 := &http.Client{Transport: &http.Transport{}} // make sure an activation works httpmock.Activate() httpmock.ActivateNonDefault(client1) httpmock.ActivateNonDefault(client2) require.Cmp(http.DefaultTransport, httpmock.DefaultTransport, "expected http.DefaultTransport to be our DefaultTransport") require.Cmp(client1.Transport, httpmock.DefaultTransport, "expected client1.Transport to be our DefaultTransport") require.Cmp(client2.Transport, httpmock.DefaultTransport, "expected client2.Transport to be our DefaultTransport") httpmock.Deactivate() require.CmpNoError(os.Setenv(envVarName, "1")) require.True(httpmock.Disabled(), "expected to be disabled") // make sure activation doesn't work httpmock.Activate() httpmock.ActivateNonDefault(client1) httpmock.ActivateNonDefault(client2) require.Not(http.DefaultTransport, httpmock.DefaultTransport, "expected http.DefaultTransport to not be our DefaultTransport") require.Not(client1.Transport, httpmock.DefaultTransport, "expected client1.Transport to not be our DefaultTransport") require.Not(client2.Transport, httpmock.DefaultTransport, "expected client2.Transport to not be our DefaultTransport") httpmock.Deactivate() } httpmock-1.3.1/export_test.go000066400000000000000000000030661446714405000162660ustar00rootroot00000000000000package httpmock import ( "io" "net/http" "reflect" "sync/atomic" "github.com/jarcoal/httpmock/internal" ) var ( GetPackage = getPackage ExtractPackage = extractPackage CalledFrom = calledFrom ) type ( MatchResponder = matchResponder MatchResponders = matchResponders ) func init() { atomic.AddInt64(&matcherID, 0xabcdef) } func GetIgnorePackages() map[string]bool { return ignorePackages } // bodyCopyOnRead func NewBodyCopyOnRead(body io.ReadCloser) *bodyCopyOnRead { //nolint: revive return &bodyCopyOnRead{body: body} } func (b *bodyCopyOnRead) Body() io.ReadCloser { return b.body } func (b *bodyCopyOnRead) Rearm() { b.rearm() } // matchRouteKey func NewMatchRouteKey(rk internal.RouteKey, name string) matchRouteKey { //nolint: revive return matchRouteKey{RouteKey: rk, name: name} } // matchResponder func NewMatchResponder(matcher Matcher, resp Responder) matchResponder { //nolint: revive return matchResponder{matcher: matcher, responder: resp} } func (mr matchResponder) ResponderPointer() uintptr { return reflect.ValueOf(mr.responder).Pointer() } func (mr matchResponder) Matcher() Matcher { return mr.matcher } // matchResponders func (mrs matchResponders) Add(mr matchResponder) matchResponders { return mrs.add(mr) } func (mrs matchResponders) Remove(name string) matchResponders { return mrs.remove(name) } func (mrs matchResponders) FindMatchResponder(req *http.Request) *matchResponder { return mrs.findMatchResponder(req) } // Matcher func (m Matcher) FnPointer() uintptr { return reflect.ValueOf(m.fn).Pointer() } httpmock-1.3.1/file.go000066400000000000000000000032321446714405000146200ustar00rootroot00000000000000package httpmock import ( "fmt" "io/ioutil" //nolint: staticcheck ) // 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 [File.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 [encoding/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 implements [fmt.Stringer] and 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.3.1/file_test.go000066400000000000000000000040041446714405000156550ustar00rootroot00000000000000package httpmock_test import ( "encoding/json" "path/filepath" "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock" ) var _ json.Marshaler = httpmock.File("test.json") func TestFile(t *testing.T) { assert := td.Assert(t) dir, cleanup := tmpDir(assert) defer cleanup() assert.Run("Valid JSON file", func(assert *td.T) { okFile := filepath.Join(dir, "ok.json") writeFile(assert, okFile, []byte(`{ "test": true }`)) encoded, err := json.Marshal(httpmock.File(okFile)) if !assert.CmpNoError(err, "json.Marshal(%s)", okFile) { return } assert.String(encoded, `{"test":true}`) }) assert.Run("Nonexistent JSON file", func(assert *td.T) { nonexistentFile := filepath.Join(dir, "nonexistent.json") _, err := json.Marshal(httpmock.File(nonexistentFile)) assert.CmpError(err, "json.Marshal(%s), error expected", nonexistentFile) }) assert.Run("Invalid JSON file", func(assert *td.T) { badFile := filepath.Join(dir, "bad.json") writeFile(assert, badFile, []byte(`[123`)) _, err := json.Marshal(httpmock.File(badFile)) assert.CmpError(err, "json.Marshal(%s), error expected", badFile) }) assert.Run("Bytes", func(assert *td.T) { file := filepath.Join(dir, "ok.raw") content := []byte(`abc123`) writeFile(assert, file, content) assert.Cmp(httpmock.File(file).Bytes(), content) }) assert.Run("Bytes panic", func(assert *td.T) { nonexistentFile := filepath.Join(dir, "nonexistent.raw") assert.CmpPanic(func() { httpmock.File(nonexistentFile).Bytes() }, td.HasPrefix("Cannot read "+nonexistentFile)) }) assert.Run("String", func(assert *td.T) { file := filepath.Join(dir, "ok.txt") content := `abc123` writeFile(assert, file, []byte(content)) assert.Cmp(httpmock.File(file).String(), content) }) assert.Run("String panic", func(assert *td.T) { nonexistentFile := filepath.Join(dir, "nonexistent.txt") assert.CmpPanic( func() { httpmock.File(nonexistentFile).String() //nolint: govet }, td.HasPrefix("Cannot read "+nonexistentFile)) }) } httpmock-1.3.1/go.mod000066400000000000000000000002241446714405000144560ustar00rootroot00000000000000module github.com/jarcoal/httpmock go 1.18 require github.com/maxatome/go-testdeep v1.12.0 require github.com/davecgh/go-spew v1.1.1 // indirect httpmock-1.3.1/go.sum000066400000000000000000000005421446714405000145060ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= httpmock-1.3.1/internal/000077500000000000000000000000001446714405000151665ustar00rootroot00000000000000httpmock-1.3.1/internal/error.go000066400000000000000000000021421446714405000166450ustar00rootroot00000000000000package 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 // ErrorNoResponderFoundMistake encapsulates a NoResponderFound // error probably due to a user error on the method or URL path. type ErrorNoResponderFoundMistake struct { Kind string // "method", "URL" or "matcher" Orig string // original wrong method/URL, without any matching responder Suggested string // suggested method/URL with a matching responder } var _ error = (*ErrorNoResponderFoundMistake)(nil) // Unwrap implements the interface needed by errors.Unwrap. func (e *ErrorNoResponderFoundMistake) Unwrap() error { return NoResponderFound } // Error implements error interface. func (e *ErrorNoResponderFoundMistake) Error() string { if e.Kind == "matcher" { return fmt.Sprintf("%s despite %s", NoResponderFound, e.Suggested, ) } return fmt.Sprintf("%[1]s for %[2]s %[3]q, but one matches %[2]s %[4]q", NoResponderFound, e.Kind, e.Orig, e.Suggested, ) } httpmock-1.3.1/internal/error_test.go000066400000000000000000000012421446714405000177040ustar00rootroot00000000000000package internal_test import ( "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock/internal" ) func TestErrorNoResponderFoundMistake(t *testing.T) { e := &internal.ErrorNoResponderFoundMistake{ Kind: "method", Orig: "pipo", Suggested: "BINGO", } td.Cmp(t, e.Error(), `no responder found for method "pipo", but one matches method "BINGO"`) td.Cmp(t, e.Unwrap(), internal.NoResponderFound) e = &internal.ErrorNoResponderFoundMistake{ Kind: "matcher", Orig: "--not--used--", Suggested: "BINGO", } td.Cmp(t, e.Error(), `no responder found despite BINGO`) td.Cmp(t, e.Unwrap(), internal.NoResponderFound) } httpmock-1.3.1/internal/route_key.go000066400000000000000000000003331446714405000175220ustar00rootroot00000000000000package 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.3.1/internal/route_key_test.go000066400000000000000000000004541446714405000205650ustar00rootroot00000000000000package internal_test import ( "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock/internal" ) func TestRouteKey(t *testing.T) { td.Cmp(t, internal.NoResponder.String(), "NO_RESPONDER") td.Cmp(t, internal.RouteKey{Method: "GET", URL: "/foo"}.String(), "GET /foo") } httpmock-1.3.1/internal/stack_tracer.go000066400000000000000000000035441446714405000201700ustar00rootroot00000000000000package 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.3.1/internal/stack_tracer_test.go000066400000000000000000000031241446714405000212210ustar00rootroot00000000000000package internal_test import ( "errors" "net/http" "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock/internal" ) func TestStackTracer(t *testing.T) { st := internal.StackTracer{} td.CmpEmpty(t, st.Error()) st = internal.StackTracer{ Err: errors.New("foo"), } td.Cmp(t, st.Error(), "foo") td.Cmp(t, st.Unwrap(), st.Err) } func TestCheckStackTracer(t *testing.T) { req, err := http.NewRequest("GET", "http://foo.bar/", nil) td.Require(t).CmpNoError(err) // no error td.CmpNoError(t, internal.CheckStackTracer(req, nil)) // Classic error err = errors.New("error") td.Cmp(t, internal.CheckStackTracer(req, err), err) // stackTracer without customFn origErr := errors.New("foo") errTracer := internal.StackTracer{ Err: origErr, } td.Cmp(t, internal.CheckStackTracer(req, errTracer), origErr) // stackTracer with nil error & without customFn errTracer = internal.StackTracer{} td.CmpNoError(t, internal.CheckStackTracer(req, errTracer)) // stackTracer var mesg string errTracer = internal.StackTracer{ Err: origErr, CustomFn: func(args ...interface{}) { mesg = args[0].(string) }, } gotErr := internal.CheckStackTracer(req, errTracer) td.Cmp(t, mesg, td.Re(`(?s)^foo\nCalled from .*[^\n]\z`)) td.Cmp(t, gotErr, origErr) // stackTracer with nil error but customFn mesg = "" errTracer = internal.StackTracer{ CustomFn: func(args ...interface{}) { mesg = args[0].(string) }, } gotErr = internal.CheckStackTracer(req, errTracer) td.Cmp(t, mesg, td.Re(`(?s)^GET http://foo\.bar/\nCalled from .*[^\n]\z`)) td.CmpNoError(t, gotErr) } httpmock-1.3.1/internal/submatches.go000066400000000000000000000006761446714405000176640ustar00rootroot00000000000000package 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.3.1/internal/submatches_test.go000066400000000000000000000012601446714405000207110ustar00rootroot00000000000000package internal_test import ( "net/http" "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock/internal" ) func TestSubmatches(t *testing.T) { req, err := http.NewRequest("GET", "/foo/bar", nil) td.Require(t).CmpNoError(err) var req2 *http.Request req2 = internal.SetSubmatches(req, nil) td.CmpShallow(t, req2, req) td.CmpNil(t, internal.GetSubmatches(req2)) req2 = internal.SetSubmatches(req, []string{}) td.Cmp(t, req2, td.Shallow(req)) td.CmpNil(t, internal.GetSubmatches(req2)) req2 = internal.SetSubmatches(req, []string{"foo", "123", "-123", "12.3"}) td.CmpNot(t, req2, td.Shallow(req)) td.CmpLen(t, internal.GetSubmatches(req2), 4) } httpmock-1.3.1/match.go000066400000000000000000000317031446714405000150010ustar00rootroot00000000000000package httpmock import ( "bytes" "fmt" "io" "io/ioutil" //nolint: staticcheck "net/http" "runtime" "strings" "sync/atomic" "github.com/jarcoal/httpmock/internal" ) var ignorePackages = map[string]bool{} func init() { IgnoreMatcherHelper() } // IgnoreMatcherHelper should be called by external helpers building // [Matcher], typically in an init() function, to avoid they appear in // the autogenerated [Matcher] names. func IgnoreMatcherHelper(skip ...int) { sk := 2 if len(skip) > 0 { sk += skip[0] } if pkg := getPackage(sk); pkg != "" { ignorePackages[pkg] = true } } // Copied from github.com/maxatome/go-testdeep/internal/trace.getPackage. func getPackage(skip int) string { if pc, _, _, ok := runtime.Caller(skip); ok { if fn := runtime.FuncForPC(pc); fn != nil { return extractPackage(fn.Name()) } } return "" } // extractPackage extracts package part from a fully qualified function name: // // "foo/bar/test.fn" → "foo/bar/test" // "foo/bar/test.X.fn" → "foo/bar/test" // "foo/bar/test.(*X).fn" → "foo/bar/test" // "foo/bar/test.(*X).fn.func1" → "foo/bar/test" // "weird" → "" // // Derived from github.com/maxatome/go-testdeep/internal/trace.SplitPackageFunc. func extractPackage(fn string) string { sp := strings.LastIndexByte(fn, '/') if sp < 0 { sp = 0 // std package } dp := strings.IndexByte(fn[sp:], '.') if dp < 0 { return "" } return fn[:sp+dp] } // calledFrom returns a string like "@PKG.FUNC() FILE:LINE". func calledFrom(skip int) string { pc := make([]uintptr, 128) npc := runtime.Callers(skip+1, pc) pc = pc[:npc] frames := runtime.CallersFrames(pc) var lastFrame runtime.Frame for { frame, more := frames.Next() // If testing package is encountered, it is too late if strings.HasPrefix(frame.Function, "testing.") { break } lastFrame = frame // Stop if httpmock is not the caller if !ignorePackages[extractPackage(frame.Function)] || !more { break } } if lastFrame.Line == 0 { return "" } return fmt.Sprintf(" @%s() %s:%d", lastFrame.Function, lastFrame.File, lastFrame.Line) } // MatcherFunc type is the function to use to check a [Matcher] // matches an incoming request. When httpmock calls a function of this // type, it is guaranteed req.Body is never nil. If req.Body is nil in // the original request, it is temporarily replaced by an instance // returning always [io.EOF] for each Read() call, during the call. type MatcherFunc func(req *http.Request) bool func matcherFuncOr(mfs []MatcherFunc) MatcherFunc { return func(req *http.Request) bool { for _, mf := range mfs { rearmBody(req) if mf(req) { return true } } return false } } func matcherFuncAnd(mfs []MatcherFunc) MatcherFunc { if len(mfs) == 0 { return nil } return func(req *http.Request) bool { for _, mf := range mfs { rearmBody(req) if !mf(req) { return false } } return true } } // Check returns true if mf is nil, otherwise it returns mf(req). func (mf MatcherFunc) Check(req *http.Request) bool { return mf == nil || mf(req) } // Or combines mf and all mfs in a new [MatcherFunc]. This new // [MatcherFunc] succeeds if one of mf or mfs succeeds. Note that as a // a nil [MatcherFunc] is considered succeeding, if mf or one of mfs // items is nil, nil is returned. func (mf MatcherFunc) Or(mfs ...MatcherFunc) MatcherFunc { if len(mfs) == 0 || mf == nil { return mf } cmfs := make([]MatcherFunc, len(mfs)+1) cmfs[0] = mf for i, cur := range mfs { if cur == nil { return nil } cmfs[i+1] = cur } return matcherFuncOr(cmfs) } // And combines mf and all mfs in a new [MatcherFunc]. This new // [MatcherFunc] succeeds if all of mf and mfs succeed. Note that a // [MatcherFunc] also succeeds if it is nil, so if mf and all mfs // items are nil, nil is returned. func (mf MatcherFunc) And(mfs ...MatcherFunc) MatcherFunc { if len(mfs) == 0 { return mf } cmfs := make([]MatcherFunc, 0, len(mfs)+1) if mf != nil { cmfs = append(cmfs, mf) } for _, cur := range mfs { if cur != nil { cmfs = append(cmfs, cur) } } return matcherFuncAnd(cmfs) } // Matcher type defines a match case. The zero Matcher{} corresponds // to the default case. Otherwise, use [NewMatcher] or any helper // building a [Matcher] like [BodyContainsBytes], [BodyContainsBytes], // [HeaderExists], [HeaderIs], [HeaderContains] or any of // [github.com/maxatome/tdhttpmock] functions. type Matcher struct { name string fn MatcherFunc // can be nil → means always true } var matcherID int64 // NewMatcher returns a [Matcher]. If name is empty and fn is non-nil, // a name is automatically generated. When fn is nil, it is a default // [Matcher]: its name can be empty. // // Automatically generated names have the form: // // ~HEXANUMBER@PKG.FUNC() FILE:LINE // // Legend: // - HEXANUMBER is a unique 10 digit hexadecimal number, always increasing; // - PKG is the NewMatcher caller package (except if // [IgnoreMatcherHelper] has been previously called, in this case it // is the caller of the caller package and so on); // - FUNC is the function name of the caller in the previous PKG package; // - FILE and LINE are the location of the call in FUNC function. func NewMatcher(name string, fn MatcherFunc) Matcher { if name == "" && fn != nil { // Auto-name the matcher name = fmt.Sprintf("~%010x%s", atomic.AddInt64(&matcherID, 1), calledFrom(1)) } return Matcher{ name: name, fn: fn, } } // BodyContainsBytes returns a [Matcher] checking that request body // contains subslice. // // The name of the returned [Matcher] is auto-generated (see [NewMatcher]). // To name it explicitly, use [Matcher.WithName] as in: // // BodyContainsBytes([]byte("foo")).WithName("10-body-contains-foo") // // See also [github.com/maxatome/tdhttpmock.Body], // [github.com/maxatome/tdhttpmock.JSONBody] and // [github.com/maxatome/tdhttpmock.XMLBody] for powerful body testing. func BodyContainsBytes(subslice []byte) Matcher { return NewMatcher("", func(req *http.Request) bool { rearmBody(req) b, err := ioutil.ReadAll(req.Body) return err == nil && bytes.Contains(b, subslice) }) } // BodyContainsString returns a [Matcher] checking that request body // contains substr. // // The name of the returned [Matcher] is auto-generated (see [NewMatcher]). // To name it explicitly, use [Matcher.WithName] as in: // // BodyContainsString("foo").WithName("10-body-contains-foo") // // See also [github.com/maxatome/tdhttpmock.Body], // [github.com/maxatome/tdhttpmock.JSONBody] and // [github.com/maxatome/tdhttpmock.XMLBody] for powerful body testing. func BodyContainsString(substr string) Matcher { return NewMatcher("", func(req *http.Request) bool { rearmBody(req) b, err := ioutil.ReadAll(req.Body) return err == nil && bytes.Contains(b, []byte(substr)) }) } // HeaderExists returns a [Matcher] checking that request contains // key header. // // The name of the returned [Matcher] is auto-generated (see [NewMatcher]). // To name it explicitly, use [Matcher.WithName] as in: // // HeaderExists("X-Custom").WithName("10-custom-exists") // // See also [github.com/maxatome/tdhttpmock.Header] for powerful // header testing. func HeaderExists(key string) Matcher { return NewMatcher("", func(req *http.Request) bool { _, ok := req.Header[key] return ok }) } // HeaderIs returns a [Matcher] checking that request contains // key header set to value. // // The name of the returned [Matcher] is auto-generated (see [NewMatcher]). // To name it explicitly, use [Matcher.WithName] as in: // // HeaderIs("X-Custom", "VALUE").WithName("10-custom-is-value") // // See also [github.com/maxatome/tdhttpmock.Header] for powerful // header testing. func HeaderIs(key, value string) Matcher { return NewMatcher("", func(req *http.Request) bool { return req.Header.Get(key) == value }) } // HeaderContains returns a [Matcher] checking that request contains key // header itself containing substr. // // The name of the returned [Matcher] is auto-generated (see [NewMatcher]). // To name it explicitly, use [Matcher.WithName] as in: // // HeaderContains("X-Custom", "VALUE").WithName("10-custom-contains-value") // // See also [github.com/maxatome/tdhttpmock.Header] for powerful // header testing. func HeaderContains(key, substr string) Matcher { return NewMatcher("", func(req *http.Request) bool { return strings.Contains(req.Header.Get(key), substr) }) } // Name returns the m's name. func (m Matcher) Name() string { return m.name } // WithName returns a new [Matcher] based on m with name name. func (m Matcher) WithName(name string) Matcher { return NewMatcher(name, m.fn) } // Check returns true if req is matched by m. func (m Matcher) Check(req *http.Request) bool { return m.fn.Check(req) } // Or combines m and all ms in a new [Matcher]. This new [Matcher] // succeeds if one of m or ms succeeds. Note that as a [Matcher] // succeeds if internal fn is nil, if m's internal fn or any of ms // item's internal fn is nil, the returned [Matcher] always // succeeds. The name of returned [Matcher] is m's one. func (m Matcher) Or(ms ...Matcher) Matcher { if len(ms) == 0 || m.fn == nil { return m } mfs := make([]MatcherFunc, 1, len(ms)+1) mfs[0] = m.fn for _, cur := range ms { if cur.fn == nil { return Matcher{} } mfs = append(mfs, cur.fn) } m.fn = matcherFuncOr(mfs) return m } // And combines m and all ms in a new [Matcher]. This new [Matcher] // succeeds if all of m and ms succeed. Note that a [Matcher] also // succeeds if [Matcher] [MatcherFunc] is nil. The name of returned // [Matcher] is m's one if the empty/default [Matcher] is returned. func (m Matcher) And(ms ...Matcher) Matcher { if len(ms) == 0 { return m } mfs := make([]MatcherFunc, 0, len(ms)+1) if m.fn != nil { mfs = append(mfs, m.fn) } for _, cur := range ms { if cur.fn != nil { mfs = append(mfs, cur.fn) } } m.fn = matcherFuncAnd(mfs) if m.fn != nil { return m } return Matcher{} } type matchResponder struct { matcher Matcher responder Responder } type matchResponders []matchResponder // add adds or replaces a matchResponder. func (mrs matchResponders) add(mr matchResponder) matchResponders { // default is always at end if mr.matcher.fn == nil { if len(mrs) > 0 && (mrs)[len(mrs)-1].matcher.fn == nil { mrs[len(mrs)-1] = mr return mrs } return append(mrs, mr) } for i, cur := range mrs { if cur.matcher.name == mr.matcher.name { mrs[i] = mr return mrs } } for i, cur := range mrs { if cur.matcher.fn == nil || cur.matcher.name > mr.matcher.name { mrs = append(mrs, matchResponder{}) copy(mrs[i+1:], mrs[i:len(mrs)-1]) mrs[i] = mr return mrs } } return append(mrs, mr) } func (mrs matchResponders) checkEmptiness() matchResponders { if len(mrs) == 0 { return nil } return mrs } func (mrs matchResponders) shrink() matchResponders { mrs[len(mrs)-1] = matchResponder{} mrs = mrs[:len(mrs)-1] return mrs.checkEmptiness() } func (mrs matchResponders) remove(name string) matchResponders { // Special case, even if default has been renamed, we consider "" // matching this default if name == "" { // default is always at end if len(mrs) > 0 && mrs[len(mrs)-1].matcher.fn == nil { return mrs.shrink() } return mrs.checkEmptiness() } for i, cur := range mrs { if cur.matcher.name == name { copy(mrs[i:], mrs[i+1:]) return mrs.shrink() } } return mrs.checkEmptiness() } func (mrs matchResponders) findMatchResponder(req *http.Request) *matchResponder { if len(mrs) == 0 { return nil } if mrs[0].matcher.fn == nil { // nil match is always the last return &mrs[0] } copyBody := &bodyCopyOnRead{body: req.Body} req.Body = copyBody defer func() { copyBody.rearm() req.Body = copyBody.body }() for _, mr := range mrs { copyBody.rearm() if mr.matcher.Check(req) { return &mr } } return nil } type matchRouteKey struct { internal.RouteKey name string } func (m matchRouteKey) String() string { if m.name == "" { return m.RouteKey.String() } return m.RouteKey.String() + " <" + m.name + ">" } func rearmBody(req *http.Request) { if req != nil { if body, ok := req.Body.(interface{ rearm() }); ok { body.rearm() } } } type buffer struct { *bytes.Reader } func (b buffer) Close() error { return nil } // bodyCopyOnRead mutates body into a buffer on first Read(), except // if body is nil or http.NoBody. In this case, EOF is returned for // each Read() and body stays untouched. type bodyCopyOnRead struct { body io.ReadCloser } func (b *bodyCopyOnRead) rearm() { if buf, ok := b.body.(buffer); ok { buf.Seek(0, io.SeekStart) //nolint:errcheck } // else b.body contains the original body, so don't touch } func (b *bodyCopyOnRead) copy() { if _, ok := b.body.(buffer); !ok && b.body != nil && b.body != http.NoBody { buf, _ := ioutil.ReadAll(b.body) b.body.Close() b.body = buffer{bytes.NewReader(buf)} } } func (b *bodyCopyOnRead) Read(p []byte) (n int, err error) { b.copy() if b.body == nil { return 0, io.EOF } return b.body.Read(p) } func (b *bodyCopyOnRead) Close() error { return nil } httpmock-1.3.1/match_test.go000066400000000000000000000330251446714405000160370ustar00rootroot00000000000000package httpmock_test import ( "bytes" "errors" "fmt" "io" "io/ioutil" //nolint: staticcheck "net/http" "reflect" "strings" "testing" "github.com/maxatome/go-testdeep/td" "github.com/jarcoal/httpmock" "github.com/jarcoal/httpmock/internal" ) func TestMatcherFunc_AndOr(t *testing.T) { ok := httpmock.MatcherFunc(func(*http.Request) bool { return true }) bad := httpmock.MatcherFunc(func(*http.Request) bool { return false }) td.CmpTrue(t, ok(nil)) td.CmpFalse(t, bad(nil)) t.Run("Or", func(t *testing.T) { td.CmpTrue(t, ok.Or(bad).Or(bad).Or(bad)(nil)) td.CmpTrue(t, bad.Or(bad).Or(bad).Or(ok)(nil)) td.CmpFalse(t, bad.Or(bad).Or(bad).Or(bad)(nil)) td.CmpNil(t, bad.Or(bad).Or(bad).Or(nil)) td.CmpNil(t, (httpmock.MatcherFunc)(nil).Or(bad).Or(bad).Or(bad)) td.CmpTrue(t, ok.Or()(nil)) }) t.Run("And", func(t *testing.T) { td.CmpTrue(t, ok.And(ok).And(ok).And(ok)(nil)) td.CmpTrue(t, ok.And(ok).And(nil).And(ok)(nil)) td.CmpFalse(t, ok.And(ok).And(bad).And(ok)(nil)) td.CmpFalse(t, bad.And(ok).And(ok).And(nil)(nil)) td.CmpTrue(t, ok.And()(nil)) td.CmpTrue(t, ok.And(nil)(nil)) td.CmpNil(t, (httpmock.MatcherFunc)(nil).And(nil).And(nil).And(nil)) td.CmpTrue(t, (httpmock.MatcherFunc)(nil).And(ok)(nil)) }) } func TestMatcherFunc_Check(t *testing.T) { ok := httpmock.MatcherFunc(func(*http.Request) bool { return true }) bad := httpmock.MatcherFunc(func(*http.Request) bool { return false }) td.CmpTrue(t, ok.Check(nil)) td.CmpTrue(t, (httpmock.MatcherFunc)(nil).Check(nil)) td.CmpFalse(t, bad.Check(nil)) } func TestNewMatcher(t *testing.T) { autogenName := td.Re(`^~[0-9a-f]{10} @.*/httpmock_test\.TestNewMatcher.*/match_test.go:\d+\z`) t.Run("NewMatcher", func(t *testing.T) { td.Cmp(t, httpmock.NewMatcher("xxx", func(*http.Request) bool { return true }), td.Struct(httpmock.Matcher{}, td.StructFields{ "name": "xxx", "fn": td.NotNil(), })) td.Cmp(t, httpmock.NewMatcher("", nil), httpmock.Matcher{}) td.Cmp(t, httpmock.NewMatcher("", func(*http.Request) bool { return true }), td.Struct(httpmock.Matcher{}, td.StructFields{ "name": autogenName, "fn": td.NotNil(), })) }) req := func(t testing.TB, body string, header ...string) *http.Request { req, err := http.NewRequest(http.MethodPost, "/", strings.NewReader(body)) td.Require(t).CmpNoError(err) req.Header.Set("Content-Type", "text/plain") for i := 0; i < len(header)-1; i += 2 { req.Header.Set(header[i], header[i+1]) } return req } reqCopyBody := func(t testing.TB, body string, header ...string) *http.Request { req := req(t, body, header...) req.Body = httpmock.NewBodyCopyOnRead(req.Body) return req } t.Run("BodyContainsBytes", func(t *testing.T) { m := httpmock.BodyContainsBytes([]byte("ip")) td.Cmp(t, m.Name(), autogenName) td.CmpTrue(t, m.Check(req(t, "pipo"))) td.CmpFalse(t, m.Check(req(t, "bingo"))) td.CmpTrue(t, m.Check(reqCopyBody(t, "pipo"))) td.CmpFalse(t, m.Check(reqCopyBody(t, "bingo"))) }) t.Run("BodyContainsString", func(t *testing.T) { m := httpmock.BodyContainsString("ip") td.Cmp(t, m.Name(), autogenName) td.CmpTrue(t, m.Check(req(t, "pipo"))) td.CmpFalse(t, m.Check(req(t, "bingo"))) td.CmpTrue(t, m.Check(reqCopyBody(t, "pipo"))) td.CmpFalse(t, m.Check(reqCopyBody(t, "bingo"))) }) t.Run("HeaderExists", func(t *testing.T) { m := httpmock.HeaderExists("X-Custom") td.Cmp(t, m.Name(), autogenName) td.CmpTrue(t, m.Check(req(t, "pipo", "X-Custom", "zzz"))) td.CmpFalse(t, m.Check(req(t, "bingo"))) }) t.Run("HeaderIs", func(t *testing.T) { m := httpmock.HeaderIs("X-Custom", "zzz") td.Cmp(t, m.Name(), autogenName) td.CmpTrue(t, m.Check(req(t, "pipo", "X-Custom", "zzz"))) td.CmpFalse(t, m.Check(req(t, "bingo", "X-Custom", "aaa"))) td.CmpFalse(t, m.Check(req(t, "bingo"))) }) t.Run("HeaderContains", func(t *testing.T) { m := httpmock.HeaderContains("X-Custom", "zzz") td.Cmp(t, m.Name(), autogenName) td.CmpTrue(t, m.Check(req(t, "pipo", "X-Custom", "aaa zzz bbb"))) td.CmpFalse(t, m.Check(req(t, "bingo"))) }) } func TestMatcher_NameWithName(t *testing.T) { autogenName := td.Re(`^~[0-9a-f]{10} @.*/httpmock_test\.TestMatcher_NameWithName.*/match_test.go:\d+\z`) t.Run("default", func(t *testing.T) { m := httpmock.NewMatcher("", nil) td.Cmp(t, m.Name(), "", "no autogen for nil fn (= default)") td.Cmp(t, m.WithName("pipo").Name(), "pipo") td.Cmp(t, m.Name(), "", "original Matcher stay untouched") td.Cmp(t, m.WithName("pipo").WithName("").Name(), "", "no autogen for nil fn") }) t.Run("non-default", func(t *testing.T) { m := httpmock.NewMatcher("xxx", func(*http.Request) bool { return true }) td.Cmp(t, m.Name(), "xxx") td.Cmp(t, m.WithName("pipo").Name(), "pipo") td.Cmp(t, m.Name(), "xxx", "original Matcher stay untouched") td.Cmp(t, m.WithName("pipo").WithName("").Name(), autogenName) }) } func TestMatcher_AndOr(t *testing.T) { ok := httpmock.MatcherFunc(func(*http.Request) bool { return true }) bad := httpmock.MatcherFunc(func(*http.Request) bool { return false }) t.Run("Or", func(t *testing.T) { m := httpmock.NewMatcher("a", ok). Or(httpmock.NewMatcher("b", bad)). Or(httpmock.NewMatcher("c", ok)) td.Cmp(t, m.Name(), "a") td.CmpTrue(t, m.Check(nil)) m = httpmock.NewMatcher("a", ok). Or(httpmock.NewMatcher("", nil)). Or(httpmock.NewMatcher("c", ok)) td.Cmp(t, m.Name(), "") td.CmpZero(t, m.FnPointer()) m = httpmock.NewMatcher("a", ok).Or() td.Cmp(t, m.Name(), "a") td.CmpTrue(t, m.Check(nil)) m = httpmock.NewMatcher("a", bad). Or(httpmock.NewMatcher("b", bad)). Or(httpmock.NewMatcher("c", ok)) td.Cmp(t, m.Name(), "a") td.CmpTrue(t, m.Check(nil)) m = httpmock.NewMatcher("a", bad). Or(httpmock.NewMatcher("b", bad)). Or(httpmock.NewMatcher("c", bad)) td.Cmp(t, m.Name(), "a") td.CmpFalse(t, m.Check(nil)) }) t.Run("And", func(t *testing.T) { m := httpmock.NewMatcher("a", ok). And(httpmock.NewMatcher("b", ok)). And(httpmock.NewMatcher("c", ok)) td.Cmp(t, m.Name(), "a") td.CmpTrue(t, m.Check(nil)) m = httpmock.NewMatcher("a", ok). And(httpmock.NewMatcher("b", bad)). And(httpmock.NewMatcher("c", ok)) td.Cmp(t, m.Name(), "a") td.CmpFalse(t, m.Check(nil)) mInit := httpmock.NewMatcher("", nil) m = mInit.And(httpmock.NewMatcher("", nil)). And(httpmock.NewMatcher("", nil)) td.Cmp(t, m.Name(), mInit.Name()) td.CmpZero(t, m.FnPointer()) m = httpmock.NewMatcher("a", ok).And() td.Cmp(t, m.Name(), "a") td.CmpTrue(t, m.Check(nil)) }) } var matchers = []httpmock.MatcherFunc{ func(*http.Request) bool { return false }, func(*http.Request) bool { return true }, } func findMatcher(fnPtr uintptr) int { if fnPtr == 0 { return -1 } for i, gm := range matchers { if fnPtr == reflect.ValueOf(gm).Pointer() { return i } } return -2 } func newMR(name string, num int) httpmock.MatchResponder { if num < 0 { // default matcher return httpmock.NewMatchResponder(httpmock.NewMatcher(name, nil), nil) } return httpmock.NewMatchResponder(httpmock.NewMatcher(name, matchers[num]), nil) } func checkMRs(t testing.TB, mrs httpmock.MatchResponders, names ...string) { td.Cmp(t, mrs, td.Smuggle( func(mrs httpmock.MatchResponders) []string { var ns []string for _, mr := range mrs { ns = append(ns, fmt.Sprintf("%s:%d", mr.Matcher().Name(), findMatcher(mr.Matcher().FnPointer()))) } return ns }, names)) } func TestMatchResponders_add_remove(t *testing.T) { var mrs httpmock.MatchResponders mrs = mrs.Add(newMR("foo", 0)) mrs = mrs.Add(newMR("bar", 0)) checkMRs(t, mrs, "bar:0", "foo:0") mrs = mrs.Add(newMR("bar", 1)) mrs = mrs.Add(newMR("", -1)) checkMRs(t, mrs, "bar:1", "foo:0", ":-1") mrs = mrs.Remove("foo") checkMRs(t, mrs, "bar:1", ":-1") mrs = mrs.Remove("foo") checkMRs(t, mrs, "bar:1", ":-1") mrs = mrs.Remove("") checkMRs(t, mrs, "bar:1") mrs = mrs.Remove("") checkMRs(t, mrs, "bar:1") mrs = mrs.Remove("bar") td.CmpNil(t, mrs) mrs = mrs.Remove("bar") td.CmpNil(t, mrs) mrs = nil mrs = mrs.Add(newMR("DEFAULT", -1)) mrs = mrs.Add(newMR("foo", 0)) checkMRs(t, mrs, "foo:0", "DEFAULT:-1") mrs = mrs.Add(newMR("bar", 0)) mrs = mrs.Add(newMR("bar", 1)) checkMRs(t, mrs, "bar:1", "foo:0", "DEFAULT:-1") mrs = mrs.Remove("") // remove DEFAULT checkMRs(t, mrs, "bar:1", "foo:0") mrs = mrs.Remove("") checkMRs(t, mrs, "bar:1", "foo:0") mrs = mrs.Remove("bar") checkMRs(t, mrs, "foo:0") mrs = mrs.Remove("foo") td.CmpNil(t, mrs) } func TestMatchResponders_findMatchResponder(t *testing.T) { newReq := func() *http.Request { req, _ := http.NewRequest("GET", "/foo", ioutil.NopCloser(bytes.NewReader([]byte(`BODY`)))) req.Header.Set("X-Foo", "bar") return req } assert := td.Assert(t). WithCmpHooks( func(a, b httpmock.MatchResponder) error { if a.Matcher().Name() != b.Matcher().Name() { return errors.New("name field mismatch") } if a.Matcher().FnPointer() != b.Matcher().FnPointer() { return errors.New("fn field mismatch") } if a.ResponderPointer() != b.ResponderPointer() { return errors.New("responder field mismatch") } return nil }) var mrs httpmock.MatchResponders resp := httpmock.NewStringResponder(200, "OK") req := newReq() assert.Nil(mrs.FindMatchResponder(req)) mrDefault := httpmock.NewMatchResponder(httpmock.Matcher{}, resp) mrs = mrs.Add(mrDefault) assert.Cmp(mrs.FindMatchResponder(req), &mrDefault) mrHeader1 := httpmock.NewMatchResponder( httpmock.NewMatcher("header-foo-zip", func(req *http.Request) bool { return req.Header.Get("X-Foo") == "zip" }), resp) mrs = mrs.Add(mrHeader1) assert.Cmp(mrs.FindMatchResponder(req), &mrDefault) mrHeader2 := httpmock.NewMatchResponder( httpmock.NewMatcher("header-foo-bar", func(req *http.Request) bool { return req.Header.Get("X-Foo") == "bar" }), resp) mrs = mrs.Add(mrHeader2) assert.Cmp(mrs.FindMatchResponder(req), &mrHeader2) mrs = mrs.Remove(mrHeader2.Matcher().Name()). Remove(mrDefault.Matcher().Name()) assert.Nil(mrs.FindMatchResponder(req)) mrBody1 := httpmock.NewMatchResponder( httpmock.NewMatcher("body-FOO", func(req *http.Request) bool { b, err := ioutil.ReadAll(req.Body) return err == nil && bytes.Equal(b, []byte("FOO")) }), resp) mrs = mrs.Add(mrBody1) req = newReq() assert.Nil(mrs.FindMatchResponder(req)) mrBody2 := httpmock.NewMatchResponder( httpmock.NewMatcher("body-BODY", func(req *http.Request) bool { b, err := ioutil.ReadAll(req.Body) return err == nil && bytes.Equal(b, []byte("BODY")) }), resp) mrs = mrs.Add(mrBody2) req = newReq() assert.Cmp(mrs.FindMatchResponder(req), &mrBody2) // The request body should still be readable b, err := ioutil.ReadAll(req.Body) assert.CmpNoError(err) assert.String(b, "BODY") } func TestMatchRouteKey(t *testing.T) { td.Cmp(t, httpmock.NewMatchRouteKey( internal.RouteKey{ Method: "GET", URL: "/foo", }, ""). String(), "GET /foo") td.Cmp(t, httpmock.NewMatchRouteKey( internal.RouteKey{ Method: "GET", URL: "/foo", }, "check-header"). String(), "GET /foo ") } func TestBodyCopyOnRead(t *testing.T) { t.Run("non-nil body", func(t *testing.T) { body := ioutil.NopCloser(bytes.NewReader([]byte(`BODY`))) bc := httpmock.NewBodyCopyOnRead(body) bc.Rearm() td.Cmp(t, body, bc.Body(), "rearm didn't touch anything") var buf [4]byte n, err := bc.Read(buf[:]) td.CmpNoError(t, err) td.Cmp(t, n, 4) td.CmpString(t, buf[:], "BODY") td.Cmp(t, body, td.Not(bc.Body()), "Original body has been copied internally") td.CmpNoError(t, bc.Body().Close()) // for coverage... :) n, err = bc.Read(buf[:]) td.Cmp(t, err, io.EOF) td.Cmp(t, n, 0) bc.Rearm() n, err = bc.Read(buf[:]) td.CmpNoError(t, err) td.Cmp(t, n, 4) td.CmpString(t, buf[:], "BODY") td.CmpNoError(t, bc.Close()) }) testCases := []struct { name string body io.ReadCloser }{ { name: "nil body", }, { name: "no body", body: http.NoBody, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { bc := httpmock.NewBodyCopyOnRead(tc.body) bc.Rearm() td.Cmp(t, tc.body, bc.Body(), "rearm didn't touch anything") var buf [4]byte n, err := bc.Read(buf[:]) td.Cmp(t, err, io.EOF) td.Cmp(t, n, 0) td.Cmp(t, bc.Body(), tc.body, "body is not altered") bc.Rearm() n, err = bc.Read(buf[:]) td.Cmp(t, err, io.EOF) td.Cmp(t, n, 0) td.Cmp(t, bc.Body(), tc.body, "body is not altered") td.CmpNoError(t, bc.Close()) }) } } func TestExtractPackage(t *testing.T) { td.Cmp(t, httpmock.ExtractPackage("foo/bar/test.fn"), "foo/bar/test") td.Cmp(t, httpmock.ExtractPackage("foo/bar/test.X.fn"), "foo/bar/test") td.Cmp(t, httpmock.ExtractPackage("foo/bar/test.(*X).fn"), "foo/bar/test") td.Cmp(t, httpmock.ExtractPackage("foo/bar/test.(*X).fn.func1"), "foo/bar/test") td.Cmp(t, httpmock.ExtractPackage("weird"), "") } func TestIgnorePackages(t *testing.T) { ignorePackages := httpmock.GetIgnorePackages() td.Cmp(t, ignorePackages, td.Len(1)) td.Cmp(t, ignorePackages, td.ContainsKey(td.HasSuffix("/httpmock"))) httpmock.IgnoreMatcherHelper() td.Cmp(t, ignorePackages, td.Len(2), "current httpmock_test package added") td.Cmp(t, ignorePackages, td.ContainsKey(td.HasSuffix("/httpmock_test"))) httpmock.IgnoreMatcherHelper(1) td.Cmp(t, ignorePackages, td.Len(3), "caller of TestIgnorePackages() → testing") td.Cmp(t, ignorePackages, td.ContainsKey("testing")) td.Cmp(t, httpmock.GetPackage(1000), "") } func TestCalledFrom(t *testing.T) { td.Cmp(t, httpmock.CalledFrom(0), td.Re(`^ @.*/httpmock_test\.TestCalledFrom\(\) .*/match_test.go:\d+\z`)) td.Cmp(t, httpmock.CalledFrom(1000), "") } httpmock-1.3.1/race_test.go000066400000000000000000000004571446714405000156600ustar00rootroot00000000000000package httpmock_test import ( "net/http" "sync" "testing" . "github.com/jarcoal/httpmock" ) func TestActivateNonDefaultRace(t *testing.T) { var wg sync.WaitGroup wg.Add(10) for i := 0; i < 10; i++ { go func() { defer wg.Done() ActivateNonDefault(&http.Client{}) }() } wg.Wait() } httpmock-1.3.1/response.go000066400000000000000000000556101446714405000155460ustar00rootroot00000000000000package 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{} type suggestedInfo struct { kind string suggested string } // suggestedMethodKeyType is used by NewNotFoundResponder(). type suggestedKeyType struct{} var suggestedKey = suggestedKeyType{} // 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(...any)) 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(...any)) 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(...any)) 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(...any)) 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 // // See also [ResponderFromMultipleResponses]. 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 } // SetContentLength returns a new [Responder] based on r that ensures // the returned [*http.Response] ContentLength field and // Content-Length header are set to the right value. // // If r returns an [*http.Response] with a nil Body or equal to // [http.NoBody], the length is always set to 0. // // If r returned response.Body implements: // // Len() int // // then the length is set to the Body.Len() returned value. All // httpmock generated bodies implement this method. Beware that // [strings.Builder], [strings.Reader], [bytes.Buffer] and // [bytes.Reader] types used with [io.NopCloser] do not implement // Len() anymore. // // Otherwise, r returned response.Body is entirely copied into an // internal buffer to get its length, then it is closed. The Body of // the [*http.Response] returned by the [Responder] returned by // SetContentLength can then be read again to return its content as // usual. But keep in mind that each time this [Responder] is called, // r is called first. So this one has to carefully handle its body: it // is highly recommended to use [NewRespBodyFromString] or // [NewRespBodyFromBytes] to set the body once (as // [NewStringResponder] and [NewBytesResponder] do behind the scene), // or to build the body each time r is called. // // The following calls are all correct: // // responder = httpmock.NewStringResponder(200, "BODY").SetContentLength() // responder = httpmock.NewBytesResponder(200, []byte("BODY")).SetContentLength() // responder = ResponderFromResponse(&http.Response{ // // build a body once, but httpmock knows how to "rearm" it once read // Body: NewRespBodyFromString("BODY"), // StatusCode: 200, // }).SetContentLength() // responder = httpmock.Responder(func(req *http.Request) (*http.Response, error) { // // build a new body for each call // return &http.Response{ // StatusCode: 200, // Body: io.NopCloser(strings.NewReader("BODY")), // }, nil // }).SetContentLength() // // But the following is not correct: // // responder = httpmock.ResponderFromResponse(&http.Response{ // StatusCode: 200, // Body: io.NopCloser(strings.NewReader("BODY")), // }).SetContentLength() // // it will only succeed for the first responder call. The following // calls will deliver responses with an empty body, as it will already // been read by the first call. func (r Responder) SetContentLength() Responder { return func(req *http.Request) (*http.Response, error) { resp, err := r(req) if err != nil { return nil, err } nr := *resp switch nr.Body { case nil: nr.Body = http.NoBody fallthrough case http.NoBody: nr.ContentLength = 0 default: bl, ok := nr.Body.(interface{ Len() int }) if !ok { copyBody := &dummyReadCloser{orig: nr.Body} bl, nr.Body = copyBody, copyBody } nr.ContentLength = int64(bl.Len()) } if nr.Header == nil { nr.Header = http.Header{} } nr.Header = nr.Header.Clone() nr.Header.Set("Content-Length", strconv.FormatInt(nr.ContentLength, 10)) return &nr, nil } } // HeaderAdd returns a new [Responder] based on r that ensures the // returned [*http.Response] includes h header. It adds each h entry // to the header. It appends to any existing values associated with // each h key. Each key is case insensitive; it is canonicalized by // [http.CanonicalHeaderKey]. // // See also [Responder.HeaderSet] and [Responder.SetContentLength]. func (r Responder) HeaderAdd(h http.Header) Responder { return func(req *http.Request) (*http.Response, error) { resp, err := r(req) if err != nil { return nil, err } nr := *resp if nr.Header == nil { nr.Header = make(http.Header, len(h)) } nr.Header = nr.Header.Clone() for k, v := range h { k = http.CanonicalHeaderKey(k) if v == nil { if _, ok := nr.Header[k]; !ok { nr.Header[k] = nil } continue } nr.Header[k] = append(nr.Header[k], v...) } return &nr, nil } } // HeaderSet returns a new [Responder] based on r that ensures the // returned [*http.Response] includes h header. It sets the header // entries associated with each h key. It replaces any existing values // associated each h key. Each key is case insensitive; it is // canonicalized by [http.CanonicalHeaderKey]. // // See also [Responder.HeaderAdd] and [Responder.SetContentLength]. func (r Responder) HeaderSet(h http.Header) Responder { return func(req *http.Request) (*http.Response, error) { resp, err := r(req) if err != nil { return nil, err } nr := *resp if nr.Header == nil { nr.Header = make(http.Header, len(h)) } nr.Header = nr.Header.Clone() for k, v := range h { k = http.CanonicalHeaderKey(k) if v == nil { nr.Header[k] = nil continue } nr.Header[k] = append([]string(nil), v...) } return &nr, nil } } // 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), // ) // } // // See also [Responder.Then]. func ResponderFromMultipleResponses(responses []*http.Response, fn ...func(...any)) 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(...any)) Responder { return func(req *http.Request) (*http.Response, error) { var extra string suggested, _ := req.Context().Value(suggestedKey).(*suggestedInfo) if suggested != nil { if suggested.kind == "matcher" { extra = fmt.Sprintf(` despite %s`, suggested.suggested) } else { extra = fmt.Sprintf(`, but one matches %s %q`, suggested.kind, suggested.suggested) } } return nil, internal.StackTracer{ CustomFn: fn, Err: fmt.Errorf("Responder not found for %s %s%s", req.Method, req.URL, extra), } } } // 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 [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 [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 [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 [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 any. Also accepts // an HTTP status code. // // To pass the content of an existing file as body use [File] as in: // // httpmock.NewJsonResponse(200, httpmock.File("body.json")) func NewJsonResponse(status int, body any) (*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 // any that is encoded to JSON) and status code. // // To pass the content of an existing file as body use [File] as in: // // httpmock.NewJsonResponder(200, httpmock.File("body.json")) func NewJsonResponder(status int, body any) (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 [File] as in: // // httpmock.NewJsonResponderOrPanic(200, httpmock.File("body.json")) func NewJsonResponderOrPanic(status int, body any) 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 any. Also accepts an HTTP // status code. // // To pass the content of an existing file as body use [File] as in: // // httpmock.NewXmlResponse(200, httpmock.File("body.xml")) func NewXmlResponse(status int, body any) (*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 // any that is encoded to XML) and status code. // // To pass the content of an existing file as body use [File] as in: // // httpmock.NewXmlResponder(200, httpmock.File("body.xml")) func NewXmlResponder(status int, body any) (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 [File] as in: // // httpmock.NewXmlResponderOrPanic(200, httpmock.File("body.xml")) func NewXmlResponderOrPanic(status int, body any) 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 [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 [File] as in: // // httpmock.NewRespBodyFromBytes(httpmock.File("body.txt").Bytes()) func NewRespBodyFromBytes(body []byte) io.ReadCloser { return &dummyReadCloser{orig: body} } type lenReadSeeker interface { io.ReadSeeker Len() int } type dummyReadCloser struct { orig any // string or []byte body lenReadSeeker // 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) case io.ReadCloser: var buf bytes.Buffer io.Copy(&buf, body) //nolint: errcheck body.Close() d.body = bytes.NewReader(buf.Bytes()) } } } 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 } func (d *dummyReadCloser) Len() int { d.setup() return d.body.Len() } httpmock-1.3.1/response_test.go000066400000000000000000000452271446714405000166100ustar00rootroot00000000000000package httpmock_test import ( "encoding/xml" "errors" "fmt" "io" "io/ioutil" //nolint: staticcheck "net/http" "path/filepath" "strconv" "strings" "sync" "testing" "time" "github.com/maxatome/go-testdeep/td" . "github.com/jarcoal/httpmock" "github.com/jarcoal/httpmock/internal" ) func TestResponderFromResponse(t *testing.T) { assert, require := td.AssertRequire(t) responder := ResponderFromResponse(NewStringResponse(200, "hello world")) req, err := http.NewRequest(http.MethodGet, testURL, nil) require.CmpNoError(err) response1, err := responder(req) require.CmpNoError(err) testURLWithQuery := testURL + "?a=1" req, err = http.NewRequest(http.MethodGet, testURLWithQuery, nil) require.CmpNoError(err) response2, err := responder(req) require.CmpNoError(err) // Body should be the same for both responses assertBody(assert, response1, "hello world") assertBody(assert, response2, "hello world") // Request should be non-nil and different for each response require.NotNil(response1.Request) assert.String(response1.Request.URL, testURL) require.NotNil(response2.Request) assert.String(response2.Request.URL, testURLWithQuery) } func TestResponderFromResponses(t *testing.T) { assert, require := td.AssertRequire(t) jsonResponse, err := NewJsonResponse(200, map[string]string{"test": "toto"}) require.CmpNoError(err) responder := ResponderFromMultipleResponses( []*http.Response{ jsonResponse, NewStringResponse(200, "hello world"), }, ) req, err := http.NewRequest(http.MethodGet, testURL, nil) require.CmpNoError(err) response1, err := responder(req) require.CmpNoError(err) testURLWithQuery := testURL + "?a=1" req, err = http.NewRequest(http.MethodGet, testURLWithQuery, nil) require.CmpNoError(err) response2, err := responder(req) require.CmpNoError(err) // Body should be the same for both responses assertBody(assert, response1, `{"test":"toto"}`) assertBody(assert, response2, "hello world") // Request should be non-nil and different for each response require.NotNil(response1.Request) assert.String(response1.Request.URL, testURL) require.NotNil(response2.Request) assert.String(response2.Request.URL, testURLWithQuery) // ensure we can't call the responder more than the number of responses it embeds _, err = responder(req) assert.String(err, "not enough responses provided: responder called 3 time(s) but 2 response(s) provided") // fn usage responder = ResponderFromMultipleResponses([]*http.Response{}, func(args ...interface{}) {}) _, err = responder(req) assert.String(err, "not enough responses provided: responder called 1 time(s) but 0 response(s) provided") if assert.Isa(err, internal.StackTracer{}) { assert.NotNil(err.(internal.StackTracer).CustomFn) } } func TestNewNotFoundResponder(t *testing.T) { assert, require := td.AssertRequire(t) responder := NewNotFoundResponder(func(args ...interface{}) {}) req, err := http.NewRequest("GET", "http://foo.bar/path", nil) require.CmpNoError(err) const title = "Responder not found for GET http://foo.bar/path" resp, err := responder(req) assert.Nil(resp) assert.String(err, title) if assert.Isa(err, internal.StackTracer{}) { assert.NotNil(err.(internal.StackTracer).CustomFn) } // nil fn responder = NewNotFoundResponder(nil) resp, err = responder(req) assert.Nil(resp) assert.String(err, title) if assert.Isa(err, internal.StackTracer{}) { assert.Nil(err.(internal.StackTracer).CustomFn) } } func TestNewStringResponse(t *testing.T) { assert, require := td.AssertRequire(t) const ( body = "hello world" status = 200 ) response := NewStringResponse(status, body) data, err := ioutil.ReadAll(response.Body) require.CmpNoError(err) assert.String(data, body) assert.Cmp(response.StatusCode, status) } func TestNewBytesResponse(t *testing.T) { assert, require := td.AssertRequire(t) const ( body = "hello world" status = 200 ) response := NewBytesResponse(status, []byte(body)) data, err := ioutil.ReadAll(response.Body) require.CmpNoError(err) assert.String(data, body) assert.Cmp(response.StatusCode, status) } func TestNewJsonResponse(t *testing.T) { assert := td.Assert(t) type schema struct { Hello string `json:"hello"` } dir, cleanup := tmpDir(assert) defer cleanup() fileName := filepath.Join(dir, "ok.json") writeFile(assert, 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}`}, } { assert.Run(fmt.Sprintf("#%d", i), func(assert *td.T) { response, err := NewJsonResponse(200, test.body) if !assert.CmpNoError(err) { return } assert.Cmp(response.StatusCode, 200) assert.Cmp(response.Header.Get("Content-Type"), "application/json") assertBody(assert, response, test.expected) }) } // Error case response, err := NewJsonResponse(200, func() {}) assert.CmpError(err) assert.Nil(response) } func checkResponder(assert *td.T, r Responder, expectedStatus int, expectedBody string) { assert.Helper() req, err := http.NewRequest(http.MethodGet, "/foo", nil) assert.FailureIsFatal().CmpNoError(err) resp, err := r(req) if !assert.CmpNoError(err, "Responder returned no error") { return } if !assert.NotNil(resp, "Responder returned a non-nil response") { return } assert.Cmp(resp.StatusCode, expectedStatus, "Status code is OK") assertBody(assert, resp, expectedBody) } func TestNewJsonResponder(t *testing.T) { assert := td.Assert(t) assert.Run("OK", func(assert *td.T) { r, err := NewJsonResponder(200, map[string]int{"foo": 42}) if assert.CmpNoError(err) { checkResponder(assert, r, 200, `{"foo":42}`) } }) assert.Run("OK file", func(assert *td.T) { dir, cleanup := tmpDir(assert) defer cleanup() fileName := filepath.Join(dir, "ok.json") writeFile(assert, fileName, []byte(`{ "foo" : 42 }`)) r, err := NewJsonResponder(200, File(fileName)) if assert.CmpNoError(err) { checkResponder(assert, r, 200, `{"foo":42}`) } }) assert.Run("Error", func(assert *td.T) { r, err := NewJsonResponder(200, func() {}) assert.CmpError(err) assert.Nil(r) }) assert.Run("OK don't panic", func(assert *td.T) { assert.CmpNotPanic( func() { r := NewJsonResponderOrPanic(200, map[string]int{"foo": 42}) checkResponder(assert, r, 200, `{"foo":42}`) }) }) assert.Run("Panic", func(assert *td.T) { assert.CmpPanic( func() { NewJsonResponderOrPanic(200, func() {}) }, td.Ignore()) }) } type schemaXML struct { Hello string `xml:"hello"` } func TestNewXmlResponse(t *testing.T) { assert := td.Assert(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(assert) defer cleanup() fileName := filepath.Join(dir, "ok.xml") writeFile(assert, fileName, b) for i, test := range []struct { body interface{} expected string }{ {body: body, expected: expectedBody}, {body: File(fileName), expected: expectedBody}, } { assert.Run(fmt.Sprintf("#%d", i), func(assert *td.T) { response, err := NewXmlResponse(200, test.body) if !assert.CmpNoError(err) { return } assert.Cmp(response.StatusCode, 200) assert.Cmp(response.Header.Get("Content-Type"), "application/xml") assertBody(assert, response, test.expected) }) } // Error case response, err := NewXmlResponse(200, func() {}) assert.CmpError(err) assert.Nil(response) } func TestNewXmlResponder(t *testing.T) { assert, require := td.AssertRequire(t) body := &schemaXML{"world"} b, err := xml.Marshal(body) require.CmpNoError(err) expectedBody := string(b) assert.Run("OK", func(assert *td.T) { r, err := NewXmlResponder(200, body) if assert.CmpNoError(err) { checkResponder(assert, r, 200, expectedBody) } }) assert.Run("OK file", func(assert *td.T) { dir, cleanup := tmpDir(assert) defer cleanup() fileName := filepath.Join(dir, "ok.xml") writeFile(assert, fileName, b) r, err := NewXmlResponder(200, File(fileName)) if assert.CmpNoError(err) { checkResponder(assert, r, 200, expectedBody) } }) assert.Run("Error", func(assert *td.T) { r, err := NewXmlResponder(200, func() {}) assert.CmpError(err) assert.Nil(r) }) assert.Run("OK don't panic", func(assert *td.T) { assert.CmpNotPanic( func() { r := NewXmlResponderOrPanic(200, body) checkResponder(assert, r, 200, expectedBody) }) }) assert.Run("Panic", func(assert *td.T) { assert.CmpPanic( func() { NewXmlResponderOrPanic(200, func() {}) }, td.Ignore()) }) } func TestNewErrorResponder(t *testing.T) { assert, require := td.AssertRequire(t) origError := errors.New("oh no") responder := NewErrorResponder(origError) req, err := http.NewRequest(http.MethodGet, testURL, nil) require.CmpNoError(err) response, err := responder(req) assert.Cmp(err, origError) assert.Nil(response) } func TestResponseBody(t *testing.T) { assert := td.Assert(t) const ( body = "hello world" status = 200 ) assert.Run("http.Response", func(assert *td.T) { for i, response := range []*http.Response{ NewBytesResponse(status, []byte(body)), NewStringResponse(status, body), } { assert.Run(fmt.Sprintf("resp #%d", i), func(assert *td.T) { assertBody(assert, response, body) assert.Cmp(response.StatusCode, status) var buf [1]byte _, err := response.Body.Read(buf[:]) assert.Cmp(err, io.EOF) }) } }) assert.Run("Responder", func(assert *td.T) { for i, responder := range []Responder{ NewBytesResponder(200, []byte(body)), NewStringResponder(200, body), } { assert.Run(fmt.Sprintf("resp #%d", i), func(assert *td.T) { req, _ := http.NewRequest("GET", "http://foo.bar", nil) response, err := responder(req) if !assert.CmpNoError(err) { return } assertBody(assert, response, body) var buf [1]byte _, err = response.Body.Read(buf[:]) assert.Cmp(err, io.EOF) }) } }) } func TestResponder(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) td.Require(t).CmpNoError(err) resp := &http.Response{} chk := func(r Responder, expectedResp *http.Response, expectedErr string) { t.Helper() gotResp, gotErr := r(req) td.CmpShallow(t, gotResp, expectedResp) var gotErrStr string if gotErr != nil { gotErrStr = gotErr.Error() } td.Cmp(t, gotErrStr, expectedErr) } called := false chkNotCalled := func() { if called { t.Helper() t.Errorf("Original responder should not be called") called = false } } chkCalled := func() { if !called { 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() td.Cmp(t, duration, td.Gte(100*time.Millisecond), "Responder is delayed") } func TestResponder_Then(t *testing.T) { assert, require := td.AssertRequire(t) req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) require.CmpNoError(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(assert *td.T, expectedLevel, expectedStack string) { assert.Helper() resp, err := rt(req) if !assert.CmpNoError(err, "Responder call") { return } b, err := ioutil.ReadAll(resp.Body) if !assert.CmpNoError(err, "Read response") { return } assert.String(b, expectedLevel) assert.Cmp(stack, expectedStack) } A, B, C := newResponder("A"), newResponder("B"), newResponder("C") D, E, F := newResponder("D"), newResponder("E"), newResponder("F") assert.Run("simple", func(assert *td.T) { // (r=A,then=B) rt = A.Then(B) chk(assert, "A", "A") chk(assert, "B", "AB") chk(assert, "B", "ABB") chk(assert, "B", "ABBB") }) stack = "" assert.Run("simple chained", func(assert *td.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(assert, "A", "A") chk(assert, "B", "AB") chk(assert, "C", "ABC") chk(assert, "D", "ABCD") chk(assert, "E", "ABCDE") chk(assert, "F", "ABCDEF") chk(assert, "F", "ABCDEFF") chk(assert, "F", "ABCDEFFF") }) stack = "" assert.Run("Then Responder as Then param", func(assert *td.T) { assert.CmpPanic( func() { A.Then(B.Then(C)) }, "Then() does not accept another Then() Responder as parameter") }) } func TestResponder_SetContentLength(t *testing.T) { assert, require := td.AssertRequire(t) req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) require.CmpNoError(err) testCases := []struct { name string r Responder expLen int }{ { name: "nil body", r: ResponderFromResponse(&http.Response{ StatusCode: 200, ContentLength: -1, }), expLen: 0, }, { name: "http.NoBody", r: ResponderFromResponse(&http.Response{ Body: http.NoBody, StatusCode: 200, ContentLength: -1, }), expLen: 0, }, { name: "string", r: NewStringResponder(200, "BODY"), expLen: 4, }, { name: "bytes", r: NewBytesResponder(200, []byte("BODY")), expLen: 4, }, { name: "from response OK", r: ResponderFromResponse(&http.Response{ Body: NewRespBodyFromString("BODY"), StatusCode: 200, ContentLength: -1, }), expLen: 4, }, { name: "custom without Len", r: func(req *http.Request) (*http.Response, error) { return &http.Response{ Body: ioutil.NopCloser(strings.NewReader("BODY")), StatusCode: 200, ContentLength: -1, }, nil }, expLen: 4, }, } for _, tc := range testCases { assert.Run(tc.name, func(assert *td.T) { sclr := tc.r.SetContentLength() for i := 1; i <= 3; i++ { assert.RunAssertRequire(fmt.Sprintf("#%d", i), func(assert, require *td.T) { resp, err := sclr(req) require.CmpNoError(err) assert.CmpLax(resp.ContentLength, tc.expLen) assert.Cmp(resp.Header.Get("Content-Length"), strconv.Itoa(tc.expLen)) }) } }) } assert.Run("error", func(assert *td.T) { resp, err := NewErrorResponder(errors.New("an error occurred")). SetContentLength()(req) assert.Nil(resp) assert.String(err, "an error occurred") }) } func TestResponder_HeaderAddSet(t *testing.T) { assert, require := td.AssertRequire(t) req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) require.CmpNoError(err) orig := NewStringResponder(200, "body") origNilHeader := ResponderFromResponse(&http.Response{ Status: "200", StatusCode: 200, Body: NewRespBodyFromString("body"), ContentLength: -1, }) // until go1.17, http.Header cannot contain nil values after a Header.Clone() clonedNil := http.Header{"Nil": nil}.Clone()["Nil"] testCases := []struct { name string orig Responder }{ {name: "orig", orig: orig}, {name: "nil header", orig: origNilHeader}, } assert.RunAssertRequire("HeaderAdd", func(assert, require *td.T) { for _, tc := range testCases { assert.RunAssertRequire(tc.name, func(assert, require *td.T) { r := tc.orig.HeaderAdd(http.Header{"foo": {"bar"}, "nil": nil}) resp, err := r(req) require.CmpNoError(err) assert.Cmp(resp.Header, http.Header{"Foo": {"bar"}, "Nil": nil}) r = r.HeaderAdd(http.Header{"foo": {"zip"}, "test": {"pipo"}}) resp, err = r(req) require.CmpNoError(err) assert.Cmp(resp.Header, http.Header{"Foo": {"bar", "zip"}, "Test": {"pipo"}, "Nil": clonedNil}) }) } resp, err := orig(req) require.CmpNoError(err) assert.Empty(resp.Header) }) assert.RunAssertRequire("HeaderSet", func(assert, require *td.T) { for _, tc := range testCases { assert.RunAssertRequire(tc.name, func(assert, require *td.T) { r := tc.orig.HeaderSet(http.Header{"foo": {"bar"}, "nil": nil}) resp, err := r(req) require.CmpNoError(err) assert.Cmp(resp.Header, http.Header{"Foo": {"bar"}, "Nil": nil}) r = r.HeaderSet(http.Header{"foo": {"zip"}, "test": {"pipo"}}) resp, err = r(req) require.CmpNoError(err) assert.Cmp(resp.Header, http.Header{"Foo": {"zip"}, "Test": {"pipo"}, "Nil": clonedNil}) }) } resp, err := orig(req) require.CmpNoError(err) assert.Empty(resp.Header) }) assert.Run("error", func(assert *td.T) { origErr := NewErrorResponder(errors.New("an error occurred")) assert.Run("HeaderAdd", func(assert *td.T) { r := origErr.HeaderAdd(http.Header{"foo": {"bar"}}) resp, err := r(req) assert.Nil(resp) assert.String(err, "an error occurred") }) assert.Run("HeaderSet", func(assert *td.T) { r := origErr.HeaderSet(http.Header{"foo": {"bar"}}) resp, err := r(req) assert.Nil(resp) assert.String(err, "an error occurred") }) }) } func TestParallelResponder(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "http://foo.bar", nil) td.Require(t).CmpNoError(err) body := strings.Repeat("ABC-", 1000) for ir, r := range []Responder{ NewStringResponder(200, body), NewBytesResponder(200, []byte(body)), } { var wg sync.WaitGroup for n := 0; n < 100; n++ { wg.Add(1) go func() { defer wg.Done() resp, _ := r(req) b, err := ioutil.ReadAll(resp.Body) td.CmpNoError(t, err, "resp #%d", ir) td.CmpLen(t, b, 4000, "resp #%d", ir) td.CmpHasPrefix(t, b, "ABC-", "resp #%d", ir) }() } wg.Wait() } } httpmock-1.3.1/transport.go000066400000000000000000001735371446714405000157550ustar00rootroot00000000000000package 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. See [RegisterNoResponder] to override this // default behavior. 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]matchResponders), callCountInfo: make(map[matchRouteKey]int), } } type regexpResponder struct { origRx string method string rx *regexp.Regexp responders matchResponders } // MockTransport implements [http.RoundTripper] interface, 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]matchResponders regexpResponders []regexpResponder noResponder Responder callCountInfo map[matchRouteKey]int totalCallCount int } var findForKey = []func(*MockTransport, internal.RouteKey) respondersFound{ (*MockTransport).respondersForKey, // Exact match (*MockTransport).regexpRespondersForKey, // Regexp match } type respondersFound struct { responders matchResponders key, respKey internal.RouteKey submatches []string } func (m *MockTransport) findResponders(method string, url *url.URL, fromIdx int) ( found respondersFound, findForKeyIndex int, ) { urlStr := url.String() key := internal.RouteKey{ Method: method, } for findForKeyIndex = fromIdx; findForKeyIndex <= len(findForKey)-1; findForKeyIndex++ { getResponders := findForKey[findForKeyIndex] // try and get a responder that matches the method and URL with // query params untouched: http://z.tld/path?q... key.URL = urlStr found = getResponders(m, key) if found.responders != nil { break } // if we weren't able to find some responders, 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) found = getResponders(m, key) if found.responders != nil { break } } // if we weren't able to find some responders, 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 found = getResponders(m, key) if found.responders != nil { break } } // if we weren't able to find some responders for the full URL, try with // the path part only pathAlone := url.RawPath if pathAlone == "" { pathAlone = url.Path } // First with unsorted querystring: /path?q... if hasQueryString { key.URL = pathAlone + strings.TrimPrefix(urlStr, surl) // concat after-path part found = getResponders(m, key) if found.responders != nil { break } // Then with sorted querystring: /path?sorted_q... key.URL = pathAlone + "?" + sortedQuery(url.Query()) if url.Fragment != "" { key.URL += "#" + url.Fragment } found = getResponders(m, key) if found.responders != nil { break } } // Then using path alone: /path key.URL = pathAlone found = getResponders(m, key) if found.responders != nil { break } } found.key = key return } // suggestResponder is typically called after a findResponders failure // to suggest a user mistake. func (m *MockTransport) suggestResponder(method string, url *url.URL) *internal.ErrorNoResponderFoundMistake { // Responder not found, try to detect some common user mistakes on // method then on path var found respondersFound // On method first if methodProbablyWrong(method) { // Get → GET found, _ = m.findResponders(strings.ToUpper(method), url, 0) } if found.responders == nil { // Search for any other method found, _ = m.findResponders("", url, 0) } if found.responders != nil { return &internal.ErrorNoResponderFoundMistake{ Kind: "method", Orig: method, Suggested: found.respKey.Method, } } // Then on path if strings.HasSuffix(url.Path, "/") { // Try without final "/" u := *url u.Path = strings.TrimSuffix(u.Path, "/") found, _ = m.findResponders("", &u, 0) } if found.responders == nil && strings.Contains(url.Path, "//") { // Try without double "/" u := *url squash := false u.Path = strings.Map(func(r rune) rune { if r == '/' { if squash { return -1 } squash = true } else { squash = false } return r }, u.Path) found, _ = m.findResponders("", &u, 0) } if found.responders != nil { return &internal.ErrorNoResponderFoundMistake{ Kind: "URL", Orig: url.String(), Suggested: found.respKey.URL, } } return nil } // 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 ( suggested *internal.ErrorNoResponderFoundMistake responder Responder fail bool found respondersFound findIdx int ) for fromFindIdx := 0; ; { found, findIdx = m.findResponders(method, req.URL, fromFindIdx) if found.responders == nil { if suggested == nil { // a suggestion is already available, no need of a new one suggested = m.suggestResponder(method, req.URL) fail = true } break } // we found some responders, check for one matcher mr := func() *matchResponder { m.mu.RLock() defer m.mu.RUnlock() return found.responders.findMatchResponder(req) }() if mr == nil { if suggested == nil { // a suggestion is not already available, do it now fail = true if len(found.responders) == 1 { suggested = &internal.ErrorNoResponderFoundMistake{ Kind: "matcher", Suggested: fmt.Sprintf("matcher %q", found.responders[0].matcher.name), } } else { names := make([]string, len(found.responders)) for i, mr := range found.responders { names[i] = mr.matcher.name } suggested = &internal.ErrorNoResponderFoundMistake{ Kind: "matcher", Suggested: fmt.Sprintf("%d matchers: %q", len(found.responders), names), } } } // No Matcher found for exact match, retry for regexp match if findIdx < len(findForKey)-1 { fromFindIdx = findIdx + 1 continue } break } // OK responder found fail = false responder = mr.responder m.mu.Lock() m.callCountInfo[matchRouteKey{RouteKey: found.key, name: mr.matcher.name}]++ if found.key != found.respKey { m.callCountInfo[matchRouteKey{RouteKey: found.respKey, name: mr.matcher.name}]++ } m.totalCallCount++ m.mu.Unlock() break } if fail { m.mu.Lock() if m.noResponder != nil { // we didn't find a responder, so fire the 'no responder' responder m.callCountInfo[matchRouteKey{RouteKey: internal.NoResponder}]++ m.totalCallCount++ // give a hint to NewNotFoundResponder() if it is a possible // method or URL error, or missing matcher if suggested != nil { req = req.WithContext(context.WithValue(req.Context(), suggestedKey, &suggestedInfo{ kind: suggested.Kind, suggested: suggested.Suggested, })) } responder = m.noResponder } m.mu.Unlock() } if responder == nil { if suggested != nil { return nil, suggested } return ConnectionFailure(req) } return runCancelable(responder, internal.SetSubmatches(req, found.submatches)) } func (m *MockTransport) numResponders() int { num := 0 for _, mrs := range m.responders { num += len(mrs) } for _, rr := range m.regexpResponders { num += len(rr.responders) } return num } // NumResponders returns the number of responders currently in use. // The responder registered with [MockTransport.RegisterNoResponder] // is not taken into account. func (m *MockTransport) NumResponders() int { m.mu.RLock() defer m.mu.RUnlock() return m.numResponders() } // Responders returns the list of currently registered responders. // Each responder is listed as a string containing "METHOD URL". // Non-regexp responders are listed first in alphabetical order // (sorted by URL then METHOD), then regexp responders in the order // they have been registered. // // The responder registered with [MockTransport.RegisterNoResponder] // is not listed. func (m *MockTransport) Responders() []string { m.mu.RLock() defer m.mu.RUnlock() rks := make([]internal.RouteKey, 0, len(m.responders)) for rk := range m.responders { rks = append(rks, rk) } sort.Slice(rks, func(i, j int) bool { if rks[i].URL == rks[j].URL { return rks[i].Method < rks[j].Method } return rks[i].URL < rks[j].URL }) rs := make([]string, 0, m.numResponders()) for _, rk := range rks { for _, mr := range m.responders[rk] { rs = append(rs, matchRouteKey{ RouteKey: rk, name: mr.matcher.name, }.String()) } } for _, rr := range m.regexpResponders { for _, mr := range rr.responders { rs = append(rs, matchRouteKey{ RouteKey: internal.RouteKey{ Method: rr.method, URL: rr.origRx, }, name: mr.matcher.name, }.String()) } } return rs } 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) } // respondersForKey returns a responder for a given key. func (m *MockTransport) respondersForKey(key internal.RouteKey) respondersFound { m.mu.RLock() defer m.mu.RUnlock() if key.Method != "" { return respondersFound{ responders: m.responders[key], respKey: key, } } for k, resp := range m.responders { if key.URL == k.URL { return respondersFound{ responders: resp, respKey: k, } } } return respondersFound{} } // respondersForKeyUsingRegexp returns the first responder matching a // given key using regexps. func (m *MockTransport) regexpRespondersForKey(key internal.RouteKey) respondersFound { 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 respondersFound{ responders: regInfo.responders, respKey: internal.RouteKey{ Method: regInfo.method, URL: regInfo.origRx, }, submatches: sm, } } } } return respondersFound{} } func isRegexpURL(url string) bool { return strings.HasPrefix(url, regexpPrefix) } func (m *MockTransport) checkMethod(method string, matcher Matcher) { 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, )) } } // RegisterMatcherResponder adds a new responder, associated with a given // HTTP method, URL (or path) and [Matcher]. // // 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 // [MockTransport.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 [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // See [MockTransport.RegisterRegexpMatcherResponder] to directly pass a // [*regexp.Regexp]. // // If several responders are registered for a same method and url // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [MockTransport.RegisterResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // 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. // // See also [MockTransport.RegisterResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func (m *MockTransport) RegisterMatcherResponder(method, url string, matcher Matcher, responder Responder) { m.checkMethod(method, matcher) mr := matchResponder{ matcher: matcher, responder: responder, } if isRegexpURL(url) { rr := regexpResponder{ origRx: url, method: method, rx: regexp.MustCompile(url[2:]), responders: matchResponders{mr}, } m.registerRegexpResponder(rr) return } key := internal.RouteKey{ Method: method, URL: url, } m.mu.Lock() if responder == nil { if mrs := m.responders[key].remove(matcher.name); mrs == nil { delete(m.responders, key) } else { m.responders[key] = mrs } delete(m.callCountInfo, matchRouteKey{RouteKey: key, name: matcher.name}) } else { m.responders[key] = m.responders[key].add(mr) m.callCountInfo[matchRouteKey{RouteKey: key, name: matcher.name}] = 0 } m.mu.Unlock() } // 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 // [MockTransport.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 [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. // // See [MockTransport.RegisterRegexpResponder] to directly pass a // [*regexp.Regexp]. // // 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. // // See [MockTransport.RegisterMatcherResponder] to also match on // request header and/or body. func (m *MockTransport) RegisterResponder(method, url string, responder Responder) { m.RegisterMatcherResponder(method, url, Matcher{}, responder) } // It is the caller responsibility that len(rxResp.reponders) == 1. func (m *MockTransport) registerRegexpResponder(rxResp regexpResponder) { m.mu.Lock() defer m.mu.Unlock() mr := rxResp.responders[0] found: for { for i, rr := range m.regexpResponders { if rr.method == rxResp.method && rr.origRx == rxResp.origRx { if mr.responder == nil { rr.responders = rr.responders.remove(mr.matcher.name) if rr.responders == 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] = rr } } else { rr.responders = rr.responders.add(mr) m.regexpResponders[i] = rr } break found } } if mr.responder != nil { m.regexpResponders = append(m.regexpResponders, rxResp) } break // nolint: staticcheck } mrk := matchRouteKey{ RouteKey: internal.RouteKey{ Method: rxResp.method, URL: rxResp.origRx, }, name: mr.matcher.name, } if mr.responder == nil { delete(m.callCountInfo, mrk) } else { m.callCountInfo[mrk] = 0 } } // RegisterRegexpMatcherResponder adds a new responder, associated // with a given HTTP method, URL (or path) regular expression and // [Matcher]. // // 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 and same // [Matcher] name) replaces its responder, but does not change its // position, and resets the corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. // // If several responders are registered for a same method and urlRegexp // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [MockTransport.RegisterRegexpResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [MockTransport.GetCallCountInfo]. // // See [MockTransport.RegisterMatcherResponder] 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. // // See [MockTransport.RegisterRegexpResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func (m *MockTransport) RegisterRegexpMatcherResponder(method string, urlRegexp *regexp.Regexp, matcher Matcher, responder Responder) { m.checkMethod(method, matcher) m.registerRegexpResponder(regexpResponder{ origRx: regexpPrefix + urlRegexp.String(), method: method, rx: urlRegexp, responders: matchResponders{{matcher: matcher, responder: 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 // [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.MockTransportGetCallCountInfo]. It does nothing if // it does not already exist. // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [MockTransport.GetCallCountInfo]. // // See [MockTransport.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. // // See [MockTransport.RegisterRegexpMatcherResponder] to also match on // request header and/or body. func (m *MockTransport) RegisterRegexpResponder(method string, urlRegexp *regexp.Regexp, responder Responder) { m.RegisterRegexpMatcherResponder(method, urlRegexp, Matcher{}, responder) } // RegisterMatcherResponderWithQuery is same as // [MockTransport.RegisterMatcherResponder], 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 [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [MockTransport.RegisterMatcherResponder], 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 [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.GetCallCountInfo]. It does nothing if it does not // already exist. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // If several responders are registered for a same method, path and // query tuple, but with different matchers, they are ordered // depending on the following rules: // - the zero matcher, Matcher{} (or responder set using // [MockTransport.RegisterResponderWithQuery]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // 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. // // See also [MockTransport.RegisterResponderWithQuery] if a matcher is // not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func (m *MockTransport) RegisterMatcherResponderWithQuery(method, path string, query any, matcher Matcher, 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.RegisterMatcherResponder(method, path, matcher, responder) } // RegisterResponderWithQuery is same as // [MockTransport.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 [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [MockTransport.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 [MockTransport.GetCallCountInfo]. // // Registering a nil [Responder] removes the existing one and the // corresponding statistics as returned by // [MockTransport.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. // // See [MockTransport.RegisterMatcherResponderWithQuery] to also match on // request header and/or body. func (m *MockTransport) RegisterResponderWithQuery(method, path string, query any, responder Responder) { m.RegisterMatcherResponderWithQuery(method, path, query, Matcher{}, 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 responders are found. The default is [ConnectionFailure] // that returns a connection error. // // Use it in conjunction with [NewNotFoundResponder] to ensure that all // routes have been mocked: // // func TestMyApp(t *testing.T) { // ... // // Calls testing.Fatal with the name of Responder-less route and // // the stack trace of the call. // mock.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 // ([ConnectionFailure]) is re-enabled. // // In some cases you may not want all URLs to be mocked, in which case // you can do this: // // func TestFetchArticles(t *testing.T) { // ... // mock.RegisterNoResponder(httpmock.InitialTransport.RoundTrip) // // // any requests that don't have a registered URL will be fetched normally // } 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]matchResponders) m.regexpResponders = nil m.noResponder = nil m.callCountInfo = make(map[matchRouteKey]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 m 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 gets the total number of calls m has taken // since it was activated or reset. func (m *MockTransport) GetTotalCallCount() int { m.mu.RLock() defer m.mu.RUnlock() return m.totalCallCount } // DefaultTransport is the default mock transport used by [Activate], // [Deactivate], [Reset], [DeactivateAndReset], [RegisterResponder], // [RegisterRegexpResponder], [RegisterResponderWithQuery] 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{} // oldClientsLock protects oldClients from concurrent writes. var oldClientsLock sync.Mutex // Activate starts the mock environment. This should be called before // your tests run. Under the hood this replaces the [http.Client.Transport] // field of [http.DefaultClient] with [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 oldClientsLock.Lock() defer oldClientsLock.Unlock() 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 oldClientsLock.Lock() defer oldClientsLock.Unlock() 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() } // RegisterMatcherResponder adds a new responder, associated with a given // HTTP method, URL (or path) and [Matcher]. // // 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. The original matcher can be passed but also a new // [Matcher] with the same name and a nil match function as in: // // NewMatcher("original matcher name", nil) // // See [RegisterRegexpMatcherResponder] to directly pass a // [*regexp.Regexp]. // // Example: // // func TestCreateArticle(t *testing.T) { // httpmock.Activate() // defer httpmock.DeactivateAndReset() // // // Mock POST /item only if `"name":"Bob"` is found in request body // httpmock.RegisterMatcherResponder("POST", "/item", // httpmock.BodyContainsString(`"name":"Bob"`), // httpmock.NewStringResponder(201, `{"id":1234}`)) // // // Can be more acurate with github.com/maxatome/tdhttpmock package // // paired with github.com/maxatome/go-testdeep/td operators as in // httpmock.RegisterMatcherResponder("POST", "/item", // tdhttpmock.JSONBody(td.JSONPointer("/name", "Alice")), // httpmock.NewStringResponder(201, `{"id":4567}`)) // // // POST requests to http://anything/item with body containing either // // `"name":"Bob"` or a JSON message with key "name" set to "Alice" // // value return the corresponding "id" response // } // // If several responders are registered for a same method and url // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [RegisterResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // 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. // // See also [RegisterResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func RegisterMatcherResponder(method, url string, matcher Matcher, responder Responder) { DefaultTransport.RegisterMatcherResponder(method, url, matcher, responder) } // 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(200, "any host hello world")) // // httpmock.RegisterResponder("GET", `=~^/item/id/\d+\z`, // httpmock.NewStringResponder(200, "any item get")) // // // 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) } // RegisterRegexpMatcherResponder adds a new responder, associated // with a given HTTP method, URL (or path) regular expression and // [Matcher]. // // 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 and same // [Matcher] name) replaces its responder, but does not change its // position, and resets the corresponding statistics as returned by // [GetCallCountInfo]. // // If several responders are registered for a same method and urlRegexp // couple, but with different matchers, they are ordered depending on // the following rules: // - the zero matcher, Matcher{} (or responder set using // [RegisterRegexpResponder]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // 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. The original matcher can be // passed but also a new [Matcher] with the same name and a nil match // function as in: // // NewMatcher("original matcher name", nil) // // A "=~" prefix is added to the stringified regexp in the statistics // returned by [GetCallCountInfo]. // // See [RegisterMatcherResponder] 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. // // See [RegisterRegexpResponder] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func RegisterRegexpMatcherResponder(method string, urlRegexp *regexp.Regexp, matcher Matcher, responder Responder) { DefaultTransport.RegisterRegexpMatcherResponder(method, urlRegexp, matcher, 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) } // RegisterMatcherResponderWithQuery is same as // [RegisterMatcherResponder], 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 [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [url.ParseQuery], a panic() occurs. // // Unlike [RegisterMatcherResponder], 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. The original matcher can be // passed but also a new [Matcher] with the same name and a nil match // function as in: // // NewMatcher("original matcher name", nil) // // If several responders are registered for a same method, path and // query tuple, but with different matchers, they are ordered // depending on the following rules: // - the zero matcher, Matcher{} (or responder set using // [.RegisterResponderWithQuery]) is always called lastly; // - other matchers are ordered by their name. If a matcher does not // have an explicit name ([NewMatcher] called with an empty name and // [Matcher.WithName] method not called), a name is automatically // computed so all anonymous matchers are sorted by their creation // order. An automatically computed name has always the form // "~HEXANUMBER@PKG.FUNC() FILE:LINE". See [NewMatcher] for details. // // 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. // // See also [RegisterResponderWithQuery] if a matcher is not needed. // // Note that [github.com/maxatome/tdhttpmock] provides powerful helpers // to create matchers with the help of [github.com/maxatome/go-testdeep]. func RegisterMatcherResponderWithQuery(method, path string, query any, matcher Matcher, responder Responder) { DefaultTransport.RegisterMatcherResponderWithQuery(method, path, query, matcher, 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 [url.ParseQuery] function) // // If the query type is not recognized or the string cannot be parsed // using [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 [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(200, "hello world")) // // // 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(200, "hello world")) // // // 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(200, "hello world")) // // // 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 any, responder Responder) { RegisterMatcherResponderWithQuery(method, path, query, Matcher{}, responder) } // RegisterNoResponder is used to register a responder that is called // if no other responders are found. The default is [ConnectionFailure] // that returns a connection error. // // 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 // ([ConnectionFailure]) is re-enabled. // // 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.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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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 (as well as [MockTransport.RegisterRegexpResponder] // or [MockTransport.RegisterResponder]). 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]any{ // "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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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 // (as well as [MockTransport.RegisterRegexpResponder] or // [MockTransport.RegisterResponder]). 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]any{ // "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.3.1/transport_test.go000066400000000000000000001032231446714405000167750ustar00rootroot00000000000000package httpmock_test import ( "bytes" "context" "encoding/json" "errors" "fmt" "io/ioutil" //nolint: staticcheck "net" "net/http" "net/url" "regexp" "strings" "testing" "time" "github.com/maxatome/go-testdeep/td" . "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/foo/bar" body := `["hello world"]` + "\n" RegisterResponder("GET", url, NewStringResponder(200, body)) RegisterResponder("GET", `=~/xxx\z`, NewStringResponder(200, body)) assert := td.Assert(t) // Read it as a simple string (ioutil.ReadAll of assertBody will // trigger io.EOF) assert.RunAssertRequire("simple", func(assert, require *td.T) { resp, err := http.Get(url) require.CmpNoError(err) assertBody(assert, resp, body) // the http client wraps our NoResponderFound error, so we just try and match on text _, err = http.Get(testURL) assert.HasSuffix(err, NoResponderFound.Error()) // Use wrongly cased method, the error should warn us req, err := http.NewRequest("Get", url, nil) require.CmpNoError(err) c := http.Client{} _, err = c.Do(req) assert.HasSuffix(err, NoResponderFound.Error()+` for method "Get", but one matches method "GET"`) // Use POST instead of GET, the error should warn us req, err = http.NewRequest("POST", url, nil) require.CmpNoError(err) _, err = c.Do(req) assert.HasSuffix(err, NoResponderFound.Error()+` for method "POST", but one matches method "GET"`) // Same using a regexp responder req, err = http.NewRequest("POST", "http://pipo.com/xxx", nil) require.CmpNoError(err) _, err = c.Do(req) assert.HasSuffix(err, NoResponderFound.Error()+` for method "POST", but one matches method "GET"`) // Use a URL with squashable "/" in path _, err = http.Get("https://github.com////foo//bar") assert.HasSuffix(err, NoResponderFound.Error()+` for URL "https://github.com////foo//bar", but one matches URL "https://github.com/foo/bar"`) // Use a URL terminated by "/" _, err = http.Get("https://github.com/foo/bar/") assert.HasSuffix(err, NoResponderFound.Error()+` for URL "https://github.com/foo/bar/", but one matches URL "https://github.com/foo/bar"`) }) // 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 := 1; i <= 2; i++ { assert.RunAssertRequire(fmt.Sprintf("try #%d", i), func(assert, require *td.T) { resp, err := http.Get(url) require.CmpNoError(err) defer resp.Body.Close() var res []string err = json.NewDecoder(resp.Body).Decode(&res) require.CmpNoError(err) assert.Cmp(res, []string{"hello world"}) }) } } func TestRegisterMatcherResponder(t *testing.T) { Activate() defer DeactivateAndReset() RegisterMatcherResponder("POST", "/foo", NewMatcher( "00-header-foo=bar", func(r *http.Request) bool { return r.Header.Get("Foo") == "bar" }, ), NewStringResponder(200, "header-foo")) RegisterMatcherResponder("POST", "/foo", NewMatcher( "01-body-BAR", func(r *http.Request) bool { b, err := ioutil.ReadAll(r.Body) return err == nil && bytes.Contains(b, []byte("BAR")) }), NewStringResponder(200, "body-BAR")) RegisterMatcherResponder("POST", "/foo", NewMatcher( "02-body-FOO", func(r *http.Request) bool { b, err := ioutil.ReadAll(r.Body) return err == nil && bytes.Contains(b, []byte("FOO")) }), NewStringResponder(200, "body-FOO")) RegisterMatcherResponder("POST", "/foo", BodyContainsString("xxx"). Or(BodyContainsString("yyy")). WithName("03-body-xxx|yyy"), NewStringResponder(200, "body-xxx|yyy")) RegisterResponder("POST", "/foo", NewStringResponder(200, "default")) RegisterNoResponder(NewNotFoundResponder(nil)) testCases := []struct { name string body string fooHeader string expectedBody string }{ { name: "header", body: "pipo", fooHeader: "bar", expectedBody: "header-foo", }, { name: "header+body=header", body: "BAR", fooHeader: "bar", expectedBody: "header-foo", }, { name: "body BAR", body: "BAR", fooHeader: "xxx", expectedBody: "body-BAR", }, { name: "body FOO", body: "FOO", expectedBody: "body-FOO", }, { name: "body xxx", body: "...xxx...", expectedBody: "body-xxx|yyy", }, { name: "body yyy", body: "...yyy...", expectedBody: "body-xxx|yyy", }, { name: "default", body: "ANYTHING", fooHeader: "zzz", expectedBody: "default", }, } assert := td.Assert(t) for _, tc := range testCases { assert.RunAssertRequire(tc.name, func(assert, require *td.T) { req, err := http.NewRequest( "POST", "http://test.com/foo", strings.NewReader(tc.body), ) require.CmpNoError(err) req.Header.Set("Content-Type", "text/plain") if tc.fooHeader != "" { req.Header.Set("Foo", tc.fooHeader) } resp, err := http.DefaultClient.Do(req) require.CmpNoError(err) assertBody(assert, resp, tc.expectedBody) }) } // Remove the default responder RegisterResponder("POST", "/foo", nil) assert.Run("not found despite 3", func(assert *td.T) { _, err := http.Post( "http://test.com/foo", "text/plain", strings.NewReader("ANYTHING"), ) assert.HasSuffix(err, `Responder not found for POST http://test.com/foo despite 4 matchers: ["00-header-foo=bar" "01-body-BAR" "02-body-FOO" "03-body-xxx|yyy"]`) }) // Remove 3 matcher responders RegisterMatcherResponder("POST", "/foo", NewMatcher("01-body-BAR", nil), nil) RegisterMatcherResponder("POST", "/foo", NewMatcher("02-body-FOO", nil), nil) RegisterMatcherResponder("POST", "/foo", NewMatcher("03-body-xxx|yyy", nil), nil) assert.Run("not found despite 1", func(assert *td.T) { _, err := http.Post( "http://test.com/foo", "text/plain", strings.NewReader("ANYTHING"), ) assert.HasSuffix(err, `Responder not found for POST http://test.com/foo despite matcher "00-header-foo=bar"`) }) // Add a regexp responder without a Matcher: as the exact match // didn't succeed because of the "00-header-foo=bar" Matcher, the // following one must be tried ans also succeed RegisterResponder("POST", "=~^/foo", NewStringResponder(200, "regexp")) assert.RunAssertRequire("default regexp", func(assert, require *td.T) { resp, err := http.Post( "http://test.com/foo", "text/plain", strings.NewReader("ANYTHING"), ) // The exact match responder "00-header-foo=bar" fails because of // its Matcher, so regexp responders have to be checked and ^/foo // has to match require.CmpNoError(err) assertBody(assert, resp, "regexp") }) // Remove the previous regexp responder RegisterResponder("POST", "=~^/foo", nil) // Add a regexp Matcher responder that should match ZIP body RegisterMatcherResponder("POST", "=~^/foo", BodyContainsString("ZIP").WithName("10-body-ZIP"), NewStringResponder(200, "body-ZIP")) assert.RunAssertRequire("regexp matcher OK", func(assert, require *td.T) { resp, err := http.Post( "http://test.com/foo", "text/plain", strings.NewReader("ZIP"), ) // The exact match responder "00-header-foo=bar" fails because of // its Matcher, so regexp responders have to be checked and ^/foo // + body ZIP has to match require.CmpNoError(err) assertBody(assert, resp, "body-ZIP") }) assert.Run("regexp matcher no match", func(assert *td.T) { _, err := http.Post( "http://test.com/foo", "text/plain", strings.NewReader("ANYTHING"), ) // The exact match responder "00-header-foo=bar" fails because of // its Matcher, so regexp responders have to be checked BUT none // match. In this case the returned error has to be the first // encountered, so the one corresponding to the exact match phase, // not the regexp one assert.HasSuffix(err, `Responder not found for POST http://test.com/foo despite matcher "00-header-foo=bar"`) }) } // We should be able to find GET handlers when using an http.Request with a // default (zero-value) .Method. func TestMockTransportDefaultMethod(t *testing.T) { assert, require := td.AssertRequire(t) Activate() defer Deactivate() const urlString = "https://github.com/" url, err := url.Parse(urlString) require.CmpNoError(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) require.CmpNoError(err) assertBody(assert, resp, body) } func TestMockTransportReset(t *testing.T) { DeactivateAndReset() td.CmpZero(t, DefaultTransport.NumResponders(), "expected no responders at this point") td.Cmp(t, DefaultTransport.Responders(), []string{}) r := NewStringResponder(200, "hey") RegisterResponder("GET", testURL, r) RegisterResponder("POST", testURL, r) RegisterResponder("PATCH", testURL, r) RegisterResponder("GET", "/pipo/bingo", r) RegisterResponder("GET", "=~/pipo/bingo", r) RegisterResponder("GET", "=~/bingo/pipo", r) td.Cmp(t, DefaultTransport.NumResponders(), 6, "expected one responder") td.Cmp(t, DefaultTransport.Responders(), []string{ // Sorted by URL then method "GET /pipo/bingo", "GET " + testURL, "PATCH " + testURL, "POST " + testURL, // Regexp responders, in the same order they have been registered "GET =~/pipo/bingo", "GET =~/bingo/pipo", }) Reset() td.CmpZero(t, DefaultTransport.NumResponders(), "expected no responders as they were just reset") td.Cmp(t, DefaultTransport.Responders(), []string{}) } func TestMockTransportNoResponder(t *testing.T) { Activate() defer DeactivateAndReset() Reset() _, err := http.Get(testURL) td.CmpError(t, err, "expected to receive a connection error due to lack of responders") RegisterNoResponder(NewStringResponder(200, "hello world")) resp, err := http.Get(testURL) if td.CmpNoError(t, err, "expected request to succeed") { assertBody(t, resp, "hello world") } // Using NewNotFoundResponder() RegisterNoResponder(NewNotFoundResponder(nil)) _, err = http.Get(testURL) td.CmpHasSuffix(t, err, "Responder not found for GET http://www.example.com/") const url = "http://www.example.com/foo/bar" RegisterResponder("POST", url, NewStringResponder(200, "hello world")) // Help the user in case a Responder exists for another method _, err = http.Get(url) td.CmpHasSuffix(t, err, `Responder not found for GET `+url+`, but one matches method "POST"`) // Help the user in case a Responder exists for another path without final "/" _, err = http.Post(url+"/", "", nil) td.CmpHasSuffix(t, err, `Responder not found for POST `+url+`/, but one matches URL "`+url+`"`) // Help the user in case a Responder exists for another path without double "/" _, err = http.Post("http://www.example.com///foo//bar", "", nil) td.CmpHasSuffix(t, err, `Responder not found for POST http://www.example.com///foo//bar, but one matches URL "`+url+`"`) } func TestMockTransportQuerystringFallback(t *testing.T) { assert := td.Assert(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"} { assert.RunAssertRequire(suffix, func(assert, require *td.T) { reqURL := testURL + suffix // make a request for the testURL with a querystring resp, err := http.Get(reqURL) require.CmpNoError(err) assertBody(assert, 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't 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", }, }, { Responder: "/hello%2fworl%64", Paths: []string{ testURL + "hello%2fworl%64?query=string&abc=zz#fragment", testURL + "hello%2fworl%64?query=string&abc=zz", testURL + "hello%2fworl%64#fragment", testURL + "hello%2fworl%64", }, }, // 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 td.CmpNoError(t, err) { 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() td.CmpNot(t, http.DefaultTransport, td.Shallow(tripper), "expected http.DefaultTransport to be a mock transport") Deactivate() td.Cmp(t, http.DefaultTransport, td.Shallow(tripper), "expected http.DefaultTransport to be dummy") } func TestMockTransportNonDefault(t *testing.T) { assert, require := td.AssertRequire(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) require.CmpNoError(err) resp, err := client.Do(req) require.CmpNoError(err) assertBody(assert, resp, body) } func TestMockTransportRespectsCancel(t *testing.T) { assert := td.Assert(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 ic, c := range cases { assert.RunAssertRequire(fmt.Sprintf("case #%d", ic), func(assert, require *td.T) { 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) require.CmpNoError(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 c.expectedErr != nil { // err is a *url.Error here, so with a Err field assert.Cmp(err, td.Smuggle("Err", td.String(c.expectedErr.Error()))) } else { assert.CmpNoError(err) } if c.expectedBody != "" { assertBody(assert, 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) td.CmpError(t, err) } func TestMockTransportCallCountReset(t *testing.T) { assert, require := td.AssertRequire(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")) RegisterMatcherResponder("POST", "=~gitlab", BodyContainsString("pipo").WithName("pipo-in-body"), NewStringResponder(200, "body")) _, err := http.Get(url) require.CmpNoError(err) buff := new(bytes.Buffer) json.NewEncoder(buff).Encode("{}") // nolint: errcheck _, err = http.Post(url2, "application/json", buff) require.CmpNoError(err) buff.Reset() json.NewEncoder(buff).Encode(`{"pipo":"bingo"}`) // nolint: errcheck _, err = http.Post(url2, "application/json", buff) require.CmpNoError(err) _, err = http.Get(url) require.CmpNoError(err) assert.Cmp(GetTotalCallCount(), 2+1+1) assert.Cmp(GetCallCountInfo(), map[string]int{ "GET " + url: 2, // Regexp match generates 2 entries: "POST " + url2: 1, // the matched call "POST =~gitlab": 1, // the regexp responder // Regexp + matcher match also generates 2 entries: "POST " + url2 + " ": 1, // matched call "POST =~gitlab ": 1, // the regexp responder with matcher }) Reset() assert.Zero(GetTotalCallCount()) assert.Empty(GetCallCountInfo()) } func TestMockTransportCallCountZero(t *testing.T) { assert, require := td.AssertRequire(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")) RegisterMatcherResponder("POST", "=~gitlab", BodyContainsString("pipo").WithName("pipo-in-body"), NewStringResponder(200, "body")) _, err := http.Get(url) require.CmpNoError(err) buff := new(bytes.Buffer) json.NewEncoder(buff).Encode("{}") // nolint: errcheck _, err = http.Post(url2, "application/json", buff) require.CmpNoError(err) buff.Reset() json.NewEncoder(buff).Encode(`{"pipo":"bingo"}`) // nolint: errcheck _, err = http.Post(url2, "application/json", buff) require.CmpNoError(err) _, err = http.Get(url) require.CmpNoError(err) assert.Cmp(GetTotalCallCount(), 2+1+1) assert.Cmp(GetCallCountInfo(), map[string]int{ "GET " + url: 2, // Regexp match generates 2 entries: "POST " + url2: 1, // the matched call "POST =~gitlab": 1, // the regexp responder // Regexp + matcher match also generates 2 entries: "POST " + url2 + " ": 1, // matched call "POST =~gitlab ": 1, // the regexp responder with matcher }) ZeroCallCounters() assert.Zero(GetTotalCallCount()) assert.Cmp(GetCallCountInfo(), map[string]int{ "GET " + url: 0, // Regexp match generates 2 entries: "POST " + url2: 0, // the matched call "POST =~gitlab": 0, // the regexp responder // Regexp + matcher match also generates 2 entries: "POST " + url2 + " ": 0, // matched call "POST =~gitlab ": 0, // the regexp responder with matcher }) // Unregister each responder RegisterResponder("GET", url, nil) RegisterResponder("POST", "=~gitlab", nil) RegisterMatcherResponder("POST", "=~gitlab", NewMatcher("pipo-in-body", nil), nil) assert.Cmp(GetCallCountInfo(), map[string]int{ // these ones remain as they are not directly related to a // registered responder but a consequence of a regexp match "POST " + url2: 0, "POST " + url2 + " ": 0, }) } func TestRegisterResponderWithQuery(t *testing.T) { assert, require := td.AssertRequire(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 { assert.Logf("query=%v URL=%s", query, url) req, err := http.NewRequest("GET", url, nil) require.CmpNoError(err) resp, err := client.Do(req) require.CmpNoError(err) assertBody(assert, 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) require.Len(GetCallCountInfo(), 0) for _, url := range test.URLs { t.Logf("query=%v URL=%s", query, url) req, err := http.NewRequest("GET", url, nil) require.CmpNoError(err) _, err = client.Do(req) assert.HasSuffix(err, "no responder found") } 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`, }, } { td.CmpPanic(t, func() { RegisterResponderWithQuery("GET", test.Path, test.Query, resp) }, td.HasPrefix(test.PanicPrefix), `RegisterResponderWithQuery + query=%v`, test.Query) } } func TestRegisterRegexpResponder(t *testing.T) { Activate() defer DeactivateAndReset() rx := regexp.MustCompile("ex.mple") RegisterRegexpResponder("POST", rx, NewStringResponder(200, "first")) // Overwrite responder RegisterRegexpResponder("POST", rx, NewStringResponder(200, "second")) resp, err := http.Post(testURL, "text/plain", strings.NewReader("PIPO")) td.Require(t).CmpNoError(err) assertBody(t, resp, "second") RegisterRegexpMatcherResponder("POST", rx, BodyContainsString("PIPO").WithName("01-body-PIPO"), NewStringResponder(200, "matcher-PIPO")) RegisterRegexpMatcherResponder("POST", rx, BodyContainsString("BINGO").WithName("02-body-BINGO"), NewStringResponder(200, "matcher-BINGO")) resp, err = http.Post(testURL, "text/plain", strings.NewReader("PIPO")) td.Require(t).CmpNoError(err) assertBody(t, resp, "matcher-PIPO") resp, err = http.Post(testURL, "text/plain", strings.NewReader("BINGO")) td.Require(t).CmpNoError(err) assertBody(t, resp, "matcher-BINGO") // Remove 01-body-PIPO matcher RegisterRegexpMatcherResponder("POST", rx, NewMatcher("01-body-PIPO", nil), nil) resp, err = http.Post(testURL, "text/plain", strings.NewReader("PIPO")) td.Require(t).CmpNoError(err) assertBody(t, resp, "second") resp, err = http.Post(testURL, "text/plain", strings.NewReader("BINGO")) td.Require(t).CmpNoError(err) assertBody(t, resp, "matcher-BINGO") // Remove 02-body-BINGO matcher RegisterRegexpMatcherResponder("POST", rx, NewMatcher("02-body-BINGO", nil), nil) resp, err = http.Post(testURL, "text/plain", strings.NewReader("BINGO")) td.Require(t).CmpNoError(err) assertBody(t, resp, "second") } func TestSubmatches(t *testing.T) { assert, require := td.AssertRequire(t) req, err := http.NewRequest("GET", "/foo/bar", nil) require.CmpNoError(err) req2 := internal.SetSubmatches(req, []string{"foo", "123", "-123", "12.3"}) assert.Run("GetSubmatch", func(assert *td.T) { _, err := GetSubmatch(req, 1) assert.Cmp(err, ErrSubmatchNotFound) _, err = GetSubmatch(req2, 5) assert.Cmp(err, ErrSubmatchNotFound) s, err := GetSubmatch(req2, 1) assert.CmpNoError(err) assert.Cmp(s, "foo") s, err = GetSubmatch(req2, 4) assert.CmpNoError(err) assert.Cmp(s, "12.3") s = MustGetSubmatch(req2, 4) assert.Cmp(s, "12.3") }) assert.Run("GetSubmatchAsInt", func(assert *td.T) { _, err := GetSubmatchAsInt(req, 1) assert.Cmp(err, ErrSubmatchNotFound) _, err = GetSubmatchAsInt(req2, 4) // not an int assert.CmpError(err) assert.Not(err, ErrSubmatchNotFound) i, err := GetSubmatchAsInt(req2, 3) assert.CmpNoError(err) assert.CmpLax(i, -123) i = MustGetSubmatchAsInt(req2, 3) assert.CmpLax(i, -123) }) assert.Run("GetSubmatchAsUint", func(assert *td.T) { _, err := GetSubmatchAsUint(req, 1) assert.Cmp(err, ErrSubmatchNotFound) _, err = GetSubmatchAsUint(req2, 3) // not a uint assert.CmpError(err) assert.Not(err, ErrSubmatchNotFound) u, err := GetSubmatchAsUint(req2, 2) assert.CmpNoError(err) assert.CmpLax(u, 123) u = MustGetSubmatchAsUint(req2, 2) assert.CmpLax(u, 123) }) assert.Run("GetSubmatchAsFloat", func(assert *td.T) { _, err := GetSubmatchAsFloat(req, 1) assert.Cmp(err, ErrSubmatchNotFound) _, err = GetSubmatchAsFloat(req2, 1) // not a float assert.CmpError(err) assert.Not(err, ErrSubmatchNotFound) f, err := GetSubmatchAsFloat(req2, 4) assert.CmpNoError(err) assert.Cmp(f, 12.3) f = MustGetSubmatchAsFloat(req2, 4) assert.Cmp(f, 12.3) }) assert.Run("GetSubmatch* panics", func(assert *td.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: ", }, } { assert.CmpPanic(test.Fn, td.HasPrefix(test.PanicPrefix), test.Name) } }) assert.RunAssertRequire("Full test", func(assert, require *td.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") require.CmpNoError(err) assertBody(assert, resp, "OK") // Check submatches assert.CmpLax(id, 123, "MustGetSubmatchAsUint") assert.Cmp(delta, 1.2, "MustGetSubmatchAsFloat") assert.Cmp(deltaStr, "1.2", "MustGetSubmatch") assert.CmpLax(inc, -5, "MustGetSubmatchAsInt") }) } func TestCheckStackTracer(t *testing.T) { assert, require := td.AssertRequire(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) require.CmpNoError(err) assertBody(assert, resp, "{}") // Check that first frame is the net/http.Get() call assert.HasPrefix(mesg, "GET https://foo.bar/\nCalled from net/http.Get()\n at ") assert.Not(mesg, td.HasSuffix("\n")) } func TestCheckMethod(t *testing.T) { mt := NewMockTransport() const expected = `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` td.CmpPanic(t, func() { mt.RegisterResponder("get", "/pipo", NewStringResponder(200, "")) }, expected) td.CmpPanic(t, func() { mt.RegisterRegexpResponder("get", regexp.MustCompile("."), NewStringResponder(200, "")) }, expected) td.CmpPanic(t, func() { mt.RegisterResponderWithQuery("get", "/pipo", url.Values(nil), NewStringResponder(200, "")) }, expected) // // No longer panics mt.DontCheckMethod = true td.CmpNotPanic(t, func() { mt.RegisterResponder("get", "/pipo", NewStringResponder(200, "")) }) td.CmpNotPanic(t, func() { mt.RegisterRegexpResponder("get", regexp.MustCompile("."), NewStringResponder(200, "")) }) td.CmpNotPanic(t, func() { mt.RegisterResponderWithQuery("get", "/pipo", url.Values(nil), NewStringResponder(200, "")) }) } httpmock-1.3.1/util_test.go000066400000000000000000000013261446714405000157170ustar00rootroot00000000000000package httpmock_test import ( "io/ioutil" //nolint: staticcheck "net/http" "os" "testing" "github.com/maxatome/go-testdeep/td" ) func assertBody(t testing.TB, resp *http.Response, expected string) bool { t.Helper() require := td.Require(t) require.NotNil(resp) defer resp.Body.Close() data, err := ioutil.ReadAll(resp.Body) require.CmpNoError(err) return td.CmpString(t, data, expected) } func tmpDir(t testing.TB) (string, func()) { t.Helper() dir, err := ioutil.TempDir("", "httpmock") td.Require(t).CmpNoError(err) return dir, func() { os.RemoveAll(dir) } } func writeFile(t testing.TB, file string, content []byte) { t.Helper() td.Require(t).CmpNoError(ioutil.WriteFile(file, content, 0644)) }