Skip to content

Commit

Permalink
Allow mocked URL matching to be glob based
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanseymour committed Feb 12, 2024
1 parent 1c2d4c6 commit b273595
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 19 deletions.
16 changes: 14 additions & 2 deletions httpx/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"net/http"

"github.com/nyaruka/gocommon/jsonx"
"github.com/nyaruka/gocommon/stringsx"
"github.com/pkg/errors"
"golang.org/x/exp/maps"
)

// MockRequestor is a requestor which can be mocked with responses for given URLs
Expand Down Expand Up @@ -38,14 +40,24 @@ func (r *MockRequestor) Do(client *http.Client, request *http.Request) (*http.Re
r.requests = append(r.requests, request)

url := request.URL.String()
mockedResponses := r.mocks[url]

// find the most specific match against this URL
match := stringsx.GlobSelect(url, maps.Keys(r.mocks)...)
mockedResponses := r.mocks[match]

if len(mockedResponses) == 0 {
panic(fmt.Sprintf("missing mock for URL %s", url))
}

// pop the next mocked response for this URL
mocked := mockedResponses[0]
r.mocks[url] = mockedResponses[1:]
remaining := mockedResponses[1:]

if len(remaining) > 0 {
r.mocks[match] = remaining
} else {
delete(r.mocks, match)
}

if mocked.Status == 0 {
return nil, errors.New("unable to connect to server")
Expand Down
52 changes: 35 additions & 17 deletions httpx/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,25 @@ func TestMockRequestor(t *testing.T) {
httpx.NewMockResponse(202, nil, []byte("this is yahoo")),
httpx.MockConnectionError,
},
"http://*": {
httpx.NewMockResponse(203, nil, []byte("this is partial")),
},
"*": {
httpx.NewMockResponse(204, nil, []byte("this is wild")),
},
server.URL + "/thing": {
httpx.NewMockResponse(203, nil, []byte("this is local")),
httpx.NewMockResponse(205, nil, []byte("this is local")),
},
})

httpx.SetRequestor(requestor1)

req1, _ := http.NewRequest("GET", "http://google.com", nil)
response1, err := httpx.Do(http.DefaultClient, req1, nil, nil)
response, err := httpx.Do(http.DefaultClient, req1, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 200, response1.StatusCode)
assert.Equal(t, 200, response.StatusCode)

body, err := io.ReadAll(response1.Body)
body, err := io.ReadAll(response.Body)
assert.NoError(t, err)
assert.Equal(t, "this is google", string(body))

Expand All @@ -54,42 +60,54 @@ func TestMockRequestor(t *testing.T) {

// request another mocked URL
req2, _ := http.NewRequest("GET", "http://yahoo.com", nil)
response2, err := httpx.Do(http.DefaultClient, req2, nil, nil)
response, err = httpx.Do(http.DefaultClient, req2, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 202, response2.StatusCode)
assert.Equal(t, 202, response.StatusCode)
assert.Equal(t, []*http.Request{req1, req2}, requestor1.Requests())

// request second mock for first URL
req3, _ := http.NewRequest("GET", "http://google.com", nil)
response3, err := httpx.Do(http.DefaultClient, req3, nil, nil)
response, err = httpx.Do(http.DefaultClient, req3, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 201, response3.StatusCode)
assert.Equal(t, 201, response.StatusCode)

// request mocked connection error
req4, _ := http.NewRequest("GET", "http://yahoo.com", nil)
response4, err := httpx.Do(http.DefaultClient, req4, nil, nil)
response, err = httpx.Do(http.DefaultClient, req4, nil, nil)
assert.EqualError(t, err, "unable to connect to server")
assert.Nil(t, response4)
assert.Nil(t, response)

// request mocked localhost request
req5, _ := http.NewRequest("GET", server.URL+"/thing", nil)
response5, err := httpx.Do(http.DefaultClient, req5, nil, nil)
response, err = httpx.Do(http.DefaultClient, req5, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 205, response.StatusCode)

// match against http://*
req6, _ := http.NewRequest("GET", "http://yahoo.com", nil)
response, err = httpx.Do(http.DefaultClient, req6, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 203, response.StatusCode)

// match against *
req7, _ := http.NewRequest("GET", "http://yahoo.com", nil)
response, err = httpx.Do(http.DefaultClient, req7, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 203, response5.StatusCode)
assert.Equal(t, 204, response.StatusCode)

assert.False(t, requestor1.HasUnused())

// panic if we've run out of mocks for a URL
req6, _ := http.NewRequest("GET", "http://google.com", nil)
assert.Panics(t, func() { httpx.Do(http.DefaultClient, req6, nil, nil) })
req8, _ := http.NewRequest("GET", "http://google.com", nil)
assert.Panics(t, func() { httpx.Do(http.DefaultClient, req8, nil, nil) })

requestor1.SetIgnoreLocal(true)

// now a request to the local server should actually get there
req7, _ := http.NewRequest("GET", server.URL+"/thing", nil)
response7, err := httpx.Do(http.DefaultClient, req7, nil, nil)
req9, _ := http.NewRequest("GET", server.URL+"/thing", nil)
response, err = httpx.Do(http.DefaultClient, req9, nil, nil)
assert.NoError(t, err)
assert.Equal(t, 200, response7.StatusCode)
assert.Equal(t, 200, response.StatusCode)
}

func TestMockRequestorMarshaling(t *testing.T) {
Expand Down
47 changes: 47 additions & 0 deletions stringsx/glob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package stringsx

import (
"sort"
"strings"
)

// GlobMatch does very simple * based glob matching where * can be the entire pattern or start or the end or both.
func GlobMatch(s, pattern string) bool {
if pattern == "" {
return s == ""
}
if pattern == "*" {
return true
}
if strings.HasPrefix(pattern, "*") && strings.HasSuffix(pattern, "*") {
return strings.Contains(s, pattern[1:len(pattern)-1])
}
if strings.HasPrefix(pattern, "*") {
return strings.HasSuffix(s, pattern[1:])
}
if strings.HasSuffix(pattern, "*") {
return strings.HasPrefix(s, pattern[0:len(pattern)-1])
}

return s == pattern
}

// GlobSelect returns the most specific matching pattern from the given set.
func GlobSelect(s string, patterns ...string) string {
matching := make([]string, 0, len(patterns))
for _, p := range patterns {
if GlobMatch(s, p) {
matching = append(matching, p)
}
}

if len(matching) == 0 {
return ""
}

Check warning on line 40 in stringsx/glob.go

View check run for this annotation

Codecov / codecov/patch

stringsx/glob.go#L39-L40

Added lines #L39 - L40 were not covered by tests

// return the longest pattern excluding * chars
sort.SliceStable(matching, func(i, j int) bool {
return len(strings.Trim(matching[i], "*")) > len(strings.Trim(matching[j], "*"))
})
return matching[0]
}
63 changes: 63 additions & 0 deletions stringsx/glob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package stringsx_test

import (
"testing"

"github.com/nyaruka/gocommon/stringsx"
"github.com/stretchr/testify/assert"
)

func TestGlobMatch(t *testing.T) {
tcs := []struct {
input string
pattern string
matches bool
}{
{"", "", true},
{"hello", "", false},
{"hello", "hello", true},
{"HELLO", "hello", false},
{"hellohello", "hello", false},
{"hello", "*hello", true},
{"hello", "*ello", true},
{"hello", "*llo", true},
{"hello", "*lo", true},
{"hello", "*o", true},
{"hello", "*", true},
{"hello", "h*", true},
{"hello", "he*", true},
{"hello", "hel*", true},
{"hello", "hello*", true},
{"hello", "*hello*", true},
{"hello", "*ell*", true},
{"hello", "*e*", true},
{"", "*", true},
{"hello", "jam*", false},
{"hello", "*jam", false},
{"hello", "*j*", false},
}

for _, tc := range tcs {
if tc.matches {
assert.True(t, stringsx.GlobMatch(tc.input, tc.pattern), "expected match for %s / %s", tc.input, tc.pattern)
} else {
assert.False(t, stringsx.GlobMatch(tc.input, tc.pattern), "unexpected match for %s / %s", tc.input, tc.pattern)
}
}
}

func TestGlobSelect(t *testing.T) {
tcs := []struct {
input string
patterns []string
selected string
}{
{"hello", []string{"*", "*ello", "hello", "hel*"}, "hello"},
{"hello", []string{"*ello", "hel*"}, "*ello"},
{"hello", []string{"*", "abc"}, "*"},
}

for _, tc := range tcs {
assert.Equal(t, tc.selected, stringsx.GlobSelect(tc.input, tc.patterns...), "select mismatch for %s / %v", tc.input, tc.patterns)
}
}

0 comments on commit b273595

Please sign in to comment.