...

Source file src/github.com/jarcoal/httpmock/transport_test.go

Documentation: github.com/jarcoal/httpmock

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

View as plain text