Skip to content

Commit

Permalink
Reimplement root package using threadsafe
Browse files Browse the repository at this point in the history
To avoid having and maintaining duplicate code, the root not-threadsafe
package is reimplemented using a globl instance of *threadsafe.Gock.

I tried to make this as non-breaking of a change as possible, but it
could not be done without some breaking changes:
* Exported types are just exposing threadsafe types. For example,
  `type MatchFunc` is changed from
	`type MatchFunc func(*http.Request, *Request) (bool, error)`
	to `type MatchFunc = threadsafe.MatchFunc`. The ergonomics of using
	these types should be unchanged, but it is technically breaking.
* Some package-level variables were exposed to allow dynamic
	configuration, like MatchersHeader. To correctly use the
	*threadsafe.Gock instance, I had to replace the var with a getter
	function and add a setter function. For getter use cases, users will
	just have to append `()` to call the function, but for setter use
	cases they will need to modify their code a little more (especially if
	they were doing something like appending to the slice).

Other notable things:
* I tried to leave as much of the original test suite as possible to
	prove that this refactor is correct. That means there are some
	unnecessarily duplicated tests between the root package and
	`threadsafe`, so there's an opportunity for cleanup.
* Some root-level tests relied on unexported symbols which are no longer
	available to those tests. Some were able to be updated using exported
	getters, but some were deleted. I believe the deleted tests were not
	providing additional value because of the above-mentioned duplication.
* To correctly maintain the getting and setting of
	http.DefaultTransport, I added "callback" methods for
	*threadsafe.Gock: DisableCallback, InterceptCallback, and
	InterceptingCallback. The root package sets these on the `var g
	*threadsafe.Gock` variable, and the functions are responsible for
	reading or writing http.DefaultTransport. Implementing this logic in
	the original functions (e.g. `gock.Disable`) proved too odd since the
	some of the functions call others. We would have to retain some
	duplicate implementation logic to run the logic in the right place, so
	the callback methods felt like the cleanest workaround.
  • Loading branch information
wendorf committed Jan 8, 2024
1 parent c03ad0e commit 73f34a1
Show file tree
Hide file tree
Showing 17 changed files with 189 additions and 1,408 deletions.
128 changes: 40 additions & 88 deletions gock.go
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
package gock

import (
"fmt"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"sync"

"github.com/h2non/gock/threadsafe"
)

var g = threadsafe.NewGock()

func init() {
g.DisableCallback = disable
g.InterceptCallback = intercept
g.InterceptingCallback = intercepting
}

// mutex is used interally for locking thread-sensitive functions.
var mutex = &sync.Mutex{}

// config global singleton store.
var config = struct {
Networking bool
NetworkingFilters []FilterRequestFunc
Observer ObserverFunc
}{}

// ObserverFunc is implemented by users to inspect the outgoing intercepted HTTP traffic
type ObserverFunc func(*http.Request, Mock)
type ObserverFunc = threadsafe.ObserverFunc

// DumpRequest is a default implementation of ObserverFunc that dumps
// the HTTP/1.x wire representation of the http request
var DumpRequest ObserverFunc = func(request *http.Request, mock Mock) {
bytes, _ := httputil.DumpRequestOut(request, true)
fmt.Println(string(bytes))
fmt.Printf("\nMatches: %v\n---\n", mock != nil)
}

// track unmatched requests so they can be tested for
var unmatchedRequests = []*http.Request{}
var DumpRequest = g.DumpRequest

// New creates and registers a new HTTP mock with
// default settings and returns the Request DSL for HTTP mock
// definition and set up.
func New(uri string) *Request {
Intercept()

res := NewResponse()
req := NewRequest()
req.URLStruct, res.Error = url.Parse(normalizeURI(uri))

// Create the new mock expectation
exp := NewMock(req, res)
Register(exp)

return req
return g.New(uri)
}

// Intercepting returns true if gock is currently able to intercept.
func Intercepting() bool {
return g.Intercepting()
}

func intercepting() bool {
mutex.Lock()
defer mutex.Unlock()
return http.DefaultTransport == DefaultTransport
Expand All @@ -60,37 +46,33 @@ func Intercepting() bool {
// Intercept enables HTTP traffic interception via http.DefaultTransport.
// If you are using a custom HTTP transport, you have to use `gock.Transport()`
func Intercept() {
if !Intercepting() {
mutex.Lock()
http.DefaultTransport = DefaultTransport
mutex.Unlock()
}
g.Intercept()
}

func intercept() {
mutex.Lock()
http.DefaultTransport = DefaultTransport
mutex.Unlock()
}

// InterceptClient allows the developer to intercept HTTP traffic using
// a custom http.Client who uses a non default http.Transport/http.RoundTripper implementation.
func InterceptClient(cli *http.Client) {
_, ok := cli.Transport.(*Transport)
if ok {
return // if transport already intercepted, just ignore it
}
trans := NewTransport()
trans.Transport = cli.Transport
cli.Transport = trans
g.InterceptClient(cli)
}

// RestoreClient allows the developer to disable and restore the
// original transport in the given http.Client.
func RestoreClient(cli *http.Client) {
trans, ok := cli.Transport.(*Transport)
if !ok {
return
}
cli.Transport = trans.Transport
g.RestoreClient(cli)
}

// Disable disables HTTP traffic interception by gock.
func Disable() {
g.Disable()
}

func disable() {
mutex.Lock()
defer mutex.Unlock()
http.DefaultTransport = NativeTransport
Expand All @@ -99,80 +81,50 @@ func Disable() {
// Off disables the default HTTP interceptors and removes
// all the registered mocks, even if they has not been intercepted yet.
func Off() {
Flush()
Disable()
g.Off()
}

// OffAll is like `Off()`, but it also removes the unmatched requests registry.
func OffAll() {
Flush()
Disable()
CleanUnmatchedRequest()
g.OffAll()
}

// Observe provides a hook to support inspection of the request and matched mock
func Observe(fn ObserverFunc) {
mutex.Lock()
defer mutex.Unlock()
config.Observer = fn
g.Observe(fn)
}

// EnableNetworking enables real HTTP networking
func EnableNetworking() {
mutex.Lock()
defer mutex.Unlock()
config.Networking = true
g.EnableNetworking()
}

// DisableNetworking disables real HTTP networking
func DisableNetworking() {
mutex.Lock()
defer mutex.Unlock()
config.Networking = false
g.DisableNetworking()
}

// NetworkingFilter determines if an http.Request should be triggered or not.
func NetworkingFilter(fn FilterRequestFunc) {
mutex.Lock()
defer mutex.Unlock()
config.NetworkingFilters = append(config.NetworkingFilters, fn)
g.NetworkingFilter(fn)
}

// DisableNetworkingFilters disables registered networking filters.
func DisableNetworkingFilters() {
mutex.Lock()
defer mutex.Unlock()
config.NetworkingFilters = []FilterRequestFunc{}
g.DisableNetworkingFilters()
}

// GetUnmatchedRequests returns all requests that have been received but haven't matched any mock
func GetUnmatchedRequests() []*http.Request {
mutex.Lock()
defer mutex.Unlock()
return unmatchedRequests
return g.GetUnmatchedRequests()
}

// HasUnmatchedRequest returns true if gock has received any requests that didn't match a mock
func HasUnmatchedRequest() bool {
return len(GetUnmatchedRequests()) > 0
return g.HasUnmatchedRequest()
}

// CleanUnmatchedRequest cleans the unmatched requests internal registry.
func CleanUnmatchedRequest() {
mutex.Lock()
defer mutex.Unlock()
unmatchedRequests = []*http.Request{}
}

func trackUnmatchedRequest(req *http.Request) {
mutex.Lock()
defer mutex.Unlock()
unmatchedRequests = append(unmatchedRequests, req)
}

func normalizeURI(uri string) string {
if ok, _ := regexp.MatchString("^http[s]?", uri); !ok {
return "http://" + uri
}
return uri
g.CleanUnmatchedRequest()
}
2 changes: 1 addition & 1 deletion gock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ func TestUnmatched(t *testing.T) {
defer after()

// clear out any unmatchedRequests from other tests
unmatchedRequests = []*http.Request{}
CleanUnmatchedRequest()

Intercept()

Expand Down
Loading

0 comments on commit 73f34a1

Please sign in to comment.