Skip to content

Commit

Permalink
reset expectation after assert (#13)
Browse files Browse the repository at this point in the history
add jsonbodypathmatcher
  • Loading branch information
Sapexoid authored Feb 22, 2023
1 parent aec2ed2 commit 4ba379f
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 7 deletions.
12 changes: 8 additions & 4 deletions httpmockserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ type MockServer interface {
// DEFAULT returns a RequestExpectation that will be executed if no other expectation matches
DEFAULT() RequestExpectation
// AssertExpectations should be called to check if all expectations have been met
// It also removes all expectations (except the default and every expectations).
// This let you reuse the same mock server for multiple tests.
AssertExpectations()
// Shutdown should be called to stop the mock server (should be deferred at the beginning of the test function)
Shutdown()
Expand Down Expand Up @@ -147,8 +149,8 @@ func NewWithOpts(t T, opts Opts) MockServer {
}

type mockServer struct {
server *httptest.Server
finisheCalled bool
server *httptest.Server
assertCalled bool

t T

Expand Down Expand Up @@ -285,7 +287,7 @@ func (s *mockServer) DEFAULT() RequestExpectation {

func (s *mockServer) AssertExpectations() {
s.t.Helper()
s.finisheCalled = true
s.assertCalled = true
var buf bytes.Buffer

unsatisfied := false
Expand Down Expand Up @@ -320,10 +322,12 @@ func (s *mockServer) AssertExpectations() {
s.t.Fatalf("\nexpectation(s) not satisfied:\n%v", buf.String())
return
}

s.expectations = nil
}

func (s *mockServer) Shutdown() {
if !s.finisheCalled {
if !s.assertCalled {
s.t.Fatalf("AssertExpectations() was not called, no expectations were checked")
return
}
Expand Down
91 changes: 88 additions & 3 deletions httpmockserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ func TestMockServer_Body(t *testing.T) {
mockServer.AssertExpectations()
})

t.Run("should match JSON path", func(t *testing.T) {
t.Run("should check JSON path contains string", func(t *testing.T) {
tMock := new(TMock)

mockServer := httpmockserver.New(tMock)
Expand Down Expand Up @@ -743,6 +743,46 @@ func TestMockServer_Body(t *testing.T) {
mockServer.AssertExpectations()
})

t.Run("should check JSON path matches string", func(t *testing.T) {
tMock := new(TMock)
tMock.On("Fatalf", mock.Anything, mock.Anything)

mockServer := httpmockserver.New(tMock)
defer mockServer.Shutdown()

mockServer.EXPECT().Post("/test").JSONPathMatches(`$.person.age`, `^\d\d\d$`).Times(1).Response(201)
mockServer.DEFAULT().Response(400)

jsonBodyStr := `{"person": {"age": "123", "name": "John"}}`
jsonBodyNoStr := `{"person": {"age": 123, "name": "John"}}`
jsonBodyNoMatch := `{"person": {"age": 1234, "name": "John"}}`
jsonBodyMissingPath := `{age": 123, "name": "John", "extra": "field"}`
jsonBodyInvalid := `{"age": 12, "name": "John"`

res := post(mockServer.BaseURL(), "/test", jsonBodyStr, nil)
check.Equal(201, res.status)

res = post(mockServer.BaseURL(), "/test", jsonBodyNoStr, nil)
check.Equal(201, res.status)

res = post(mockServer.BaseURL(), "/test", jsonBodyNoMatch, nil)
check.Equal(400, res.status)

res = post(mockServer.BaseURL(), "/test", jsonBodyMissingPath, nil)
check.Equal(400, res.status)

res = post(mockServer.BaseURL(), "/test", jsonBodyInvalid, nil)
check.Equal(400, res.status)

res = post(mockServer.BaseURL(), "/test", "", nil)
check.Equal(400, res.status)

res = post(mockServer.BaseURL(), "/test", "nil", nil)
check.Equal(400, res.status)

mockServer.AssertExpectations()
})

t.Run("should execute bodyfunc", func(t *testing.T) {
tMock := new(TMock)
tMock.On("Fatalf", mock.Anything, mock.Anything)
Expand Down Expand Up @@ -818,9 +858,10 @@ func TestMockServer_ResponseExpectation(t *testing.T) {

mockServer.EXPECT().Get("/test").Times(1).Response(201).Body([]byte("Hello World!")).Header("Content-Type", "text/plain")
mockServer.EXPECT().Get("/test2").Times(1).Response(202).StringBody("Hello World!").Headers(Headers{"Content-Type": "text/plain"})
mockServer.EXPECT().Get("/test3").Times(1).Response(203).JsonBody(map[string]string{"hello": "world"})
mockServer.EXPECT().Get("/test3").Times(1).Response(203).ContentType("text/plain").JsonBody(map[string]string{"hello": "world"})
mockServer.EXPECT().Get("/test4").Times(1).Response(204).JsonBody(nil)
mockServer.EXPECT().Get("/test4").Times(1).Response(205).JsonBody("wrong json body")
mockServer.EXPECT().Get("/test5").Times(1).Response(205).JsonBody([]byte(`{"hello":"world"}`))
mockServer.EXPECT().Get("/test6").Times(1).Response(206).JsonBody("wrong json body")
mockServer.DEFAULT().Response(400)

res := get(mockServer.BaseURL(), "/test", nil)
Expand All @@ -834,14 +875,58 @@ func TestMockServer_ResponseExpectation(t *testing.T) {

res = get(mockServer.BaseURL(), "/test3", nil)
check.Equal(203, res.status)
check.Equal("text/plain", res.header["Content-Type"][0])
check.Equal(`{"hello":"world"}`, res.body)

res = get(mockServer.BaseURL(), "/test4", nil)
check.Equal(204, res.status)
check.Equal("", res.body)

res = get(mockServer.BaseURL(), "/test5", nil)
check.Equal(205, res.status)
// content-type is application/json
check.Equal("application/json", res.header["Content-Type"][0])
check.Equal(`{"hello":"world"}`, res.body)

mockServer.AssertExpectations()
})
}

func TestMockServer_AssertExpectations(t *testing.T) {
check := assert.New(t)

tMock := new(TMock)
mockServer := httpmockserver.New(tMock)
mockServer.DEFAULT().Response(400)
mockServer.EVERY().Header("Content-Type", "application/json")
defer mockServer.Shutdown()

t.Run("should have a get anytimes expectation", func(t *testing.T) {
mockServer.EXPECT().Get("/test").AnyTimes().Response(201)

res := get(mockServer.BaseURL(), "/test", map[string]string{"Content-Type": "application/json"})
check.Equal(201, res.status)

mockServer.AssertExpectations()
})

t.Run("should trigger default expectation after AssertExpectations", func(t *testing.T) {

res := get(mockServer.BaseURL(), "/test", map[string]string{"Content-Type": "application/json"})
check.Equal(400, res.status)

mockServer.AssertExpectations()
})

t.Run("should still use every expectation", func(t *testing.T) {
tMock.On("Errorf", mock.Anything, mock.Anything)

res := get(mockServer.BaseURL(), "/test", nil)
check.Equal(400, res.status)

mockServer.AssertExpectations()
tMock.AssertExpectations(t)
})
}

type Headers map[string]string
Expand Down
7 changes: 7 additions & 0 deletions request_expectation.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ type RequestExpectation interface {
// JSONPathContains expects a given request with a body containing a specific json value using jsonPath notation
// see: https://github.com/oliveagle/jsonpath
JSONPathContains(jsonPath string, value interface{}) RequestExpectation
// JSONPathMatches expects a given request with a body matching a regex of a value retrieved by jsonPath notation
// see: https://github.com/oliveagle/jsonpath
JSONPathMatches(jsonPath string, regex string) RequestExpectation

// BodyFunc expects a given request with a custom validation function
// you can use the provided body to do arbitrary validation
Expand Down Expand Up @@ -397,6 +400,10 @@ func (exp *requestExpectation) JSONPathContains(jsonPath string, value interface
return exp.appendValidation(jsonPathContainsValidation(jsonPath, value), "JSONPathContains: "+jsonPath)
}

func (exp *requestExpectation) JSONPathMatches(jsonPath string, regex string) RequestExpectation {
return exp.appendValidation(jsonPathMatchesValidation(jsonPath, regex), "JSONPathMatches: "+jsonPath)
}

func (exp *requestExpectation) StringBody(body string) RequestExpectation {
return exp.Body([]byte(body))
}
Expand Down
28 changes: 28 additions & 0 deletions validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,34 @@ var (
}
}

jsonPathMatchesValidation = func(jsPath string, regex string) RequestValidationFunc {
return func(in *IncomingRequest) error {
var jsBodyObject map[string]interface{}
err := json.Unmarshal(in.Body, &jsBodyObject)
if err != nil {
return fmt.Errorf("request validation failed: could not parse json body %+v: %v", in.Body, err)
}

res, err := jsonpath.JsonPathLookup(jsBodyObject, jsPath)
if err != nil {
return fmt.Errorf("request validation failed: could not find json path %v in body %+v: %v", jsPath, in.Body, err)
}

// stringify the result
var str = ""
str, ok := res.(string)
if !ok {
str = fmt.Sprintf("%v", res)
}

if regexp.MustCompile(regex).MatchString(str) {
return nil
}

return fmt.Errorf("request validation failed: json path %v should match %v but was %+v", jsPath, regex, res)
}
}

basicAuthValidation = func(user, password string) RequestValidationFunc {
return func(in *IncomingRequest) error {
_user, _password, ok := in.R.BasicAuth()
Expand Down

0 comments on commit 4ba379f

Please sign in to comment.