From a05ca1ca9c04f9e43731b15bd43441571c89e0e7 Mon Sep 17 00:00:00 2001 From: Miguel Elias dos Santos Date: Mon, 12 Jul 2021 22:27:10 +1000 Subject: [PATCH 1/4] Add draft for mock http client for unittesting --- mock/client.go | 116 ++++++++++++++++++++++++++++++++++++++++++++ mock/client_test.go | 59 ++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 mock/client.go create mode 100644 mock/client_test.go diff --git a/mock/client.go b/mock/client.go new file mode 100644 index 00000000000..53eac10c853 --- /dev/null +++ b/mock/client.go @@ -0,0 +1,116 @@ +package mock + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" +) + +type EndpointPattern = *regexp.Regexp + +// Users +var UsersGetEndpoint EndpointPattern = regexp.MustCompile(`^/users/[a-z]+`) + +// Orgs +var OrgsListEndpoint = regexp.MustCompile(`^\/users\/([a-z]+\/orgs|orgs)$`) +var OrgsGetEndpoint = regexp.MustCompile(`^/orgs/[a-z]+`) + +type RequestMatch struct { + EndpointPattern EndpointPattern + Method string // GET or POST +} + +func (rm *RequestMatch) Match(r *http.Request) bool { + if r.Method == rm.Method && rm.EndpointPattern.MatchString(r.URL.Path) { + return true + } + + return false +} + +var RequestMatchUsersGet = RequestMatch{ + EndpointPattern: UsersGetEndpoint, + Method: http.MethodGet, +} + +var RequestMatchOrganizationsList = RequestMatch{ + EndpointPattern: OrgsListEndpoint, + Method: http.MethodGet, +} + +type MockRoundTripper struct { + RequestMocks map[RequestMatch][][]byte +} + +// RoundTrip implements http.RoundTripper interface +func (mrt *MockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + for requestMatch, respBodies := range mrt.RequestMocks { + if requestMatch.Match(r) { + resp := respBodies[0] + + defer func(mrt *MockRoundTripper, rm RequestMatch) { + mrt.RequestMocks[rm] = mrt.RequestMocks[rm][1:] + }(mrt, requestMatch) + + re := bytes.NewReader(resp) + + return &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(re), + }, nil + } + } + + return nil, fmt.Errorf( + "couldn find a mock request that matches the request sent to: %s", + r.URL.Path, + ) + +} + +var _ http.RoundTripper = &MockRoundTripper{} + +type MockHttpClientOption func(*MockRoundTripper) + +func WithRequestMatch( + rm RequestMatch, + marshalled []byte, +) MockHttpClientOption { + return func(mrt *MockRoundTripper) { + if _, found := mrt.RequestMocks[rm]; !found { + mrt.RequestMocks[rm] = make([][]byte, 0) + } + + mrt.RequestMocks[rm] = append( + mrt.RequestMocks[rm], + marshalled, + ) + } +} + +func NewMockHttpClient(options ...MockHttpClientOption) *http.Client { + rt := &MockRoundTripper{ + RequestMocks: make(map[RequestMatch][][]byte), + } + + for _, o := range options { + o(rt) + } + + return &http.Client{ + Transport: rt, + } +} + +func MustMarshall(v interface{}) []byte { + b, err := json.Marshal(v) + + if err == nil { + return b + } + + panic(err) +} diff --git a/mock/client_test.go b/mock/client_test.go new file mode 100644 index 00000000000..7b5cfd22f10 --- /dev/null +++ b/mock/client_test.go @@ -0,0 +1,59 @@ +package mock + +import ( + "context" + "testing" + + "github.com/google/go-github/v37/github" +) + +func TestMockClient(t *testing.T) { + ctx := context.Background() + + mockedHttpClient := NewMockHttpClient( + WithRequestMatch( + RequestMatchUsersGet, + MustMarshall(github.User{ + Name: github.String("foobar"), + }), + ), + WithRequestMatch( + RequestMatchOrganizationsList, + MustMarshall([]github.Organization{ + { + Name: github.String("foobar123thisorgwasmocked"), + }, + }), + ), + ) + + c := github.NewClient(mockedHttpClient) + + user, _, userErr := c.Users.Get(ctx, "someUser") + + if user == nil || user.Name == nil || *user.Name != "foobar" { + t.Fatalf("User name is %s, want foobar", user) + } + + if userErr != nil { + t.Errorf("User err is %s, want nil", userErr.Error()) + } + + orgs, _, err := c.Organizations.List( + ctx, + *user.Name, + nil, + ) + + if len(orgs) != 1 { + t.Errorf("Orgs len is %d want 1", len(orgs)) + } + + if err != nil { + t.Errorf("Err is %s, want nil", err.Error()) + } + + if *(orgs[0].Name) != "foobar123thisorgwasmocked" { + t.Errorf("orgs[0].Name is %s, want %s", *orgs[0].Name, "foobar123thisorgdoesnotexist") + } +} From 6ed3ab73157c6de2dfc3cc279d1ea7940e1d6cbd Mon Sep 17 00:00:00 2001 From: Miguel Elias dos Santos Date: Mon, 12 Jul 2021 22:58:44 +1000 Subject: [PATCH 2/4] Use ioutil for NopCloser --- mock/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mock/client.go b/mock/client.go index 53eac10c853..ec4fc86b629 100644 --- a/mock/client.go +++ b/mock/client.go @@ -4,7 +4,7 @@ import ( "bytes" "encoding/json" "fmt" - "io" + "io/ioutil" "net/http" "regexp" ) @@ -59,7 +59,7 @@ func (mrt *MockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) return &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(re), + Body: ioutil.NopCloser(re), }, nil } } From 804c61ee07b807738b9f1f4ccab6970eff48122e Mon Sep 17 00:00:00 2001 From: Miguel Elias dos Santos Date: Tue, 13 Jul 2021 15:07:37 +1000 Subject: [PATCH 3/4] Fix unittests for draft mock http client --- mock/client.go | 36 +++++++++++++++++++++++++++++++++--- mock/client_test.go | 2 +- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/mock/client.go b/mock/client.go index ec4fc86b629..0a869c51aa3 100644 --- a/mock/client.go +++ b/mock/client.go @@ -12,11 +12,11 @@ import ( type EndpointPattern = *regexp.Regexp // Users -var UsersGetEndpoint EndpointPattern = regexp.MustCompile(`^/users/[a-z]+`) +var UsersGetEndpoint EndpointPattern = regexp.MustCompile(`^\/users\/[a-zA-Z]+`) // Orgs var OrgsListEndpoint = regexp.MustCompile(`^\/users\/([a-z]+\/orgs|orgs)$`) -var OrgsGetEndpoint = regexp.MustCompile(`^/orgs/[a-z]+`) +var OrgsGetEndpoint = regexp.MustCompile(`^\/orgs\/[a-z]+`) type RequestMatch struct { EndpointPattern EndpointPattern @@ -24,7 +24,8 @@ type RequestMatch struct { } func (rm *RequestMatch) Match(r *http.Request) bool { - if r.Method == rm.Method && rm.EndpointPattern.MatchString(r.URL.Path) { + if (r.Method == rm.Method) && + r.URL.Path == rm.EndpointPattern.FindString(r.URL.Path) { return true } @@ -49,6 +50,35 @@ type MockRoundTripper struct { func (mrt *MockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { for requestMatch, respBodies := range mrt.RequestMocks { if requestMatch.Match(r) { + if len(respBodies) == 0 { + fmt.Printf( + "no more available mocked responses for endpoit %s\n", + r.URL.Path, + ) + + fmt.Println("please add the required RequestMatch to the MockHttpClient. Eg.") + fmt.Println(` + mockedHttpClient := NewMockHttpClient( + WithRequestMatch( + RequestMatchUsersGet, + MustMarshall(github.User{ + Name: github.String("foobar"), + }), + ), + WithRequestMatch( + RequestMatchOrganizationsList, + MustMarshall([]github.Organization{ + { + Name: github.String("foobar123"), + }, + }), + ), + ) + `) + + panic(nil) + } + resp := respBodies[0] defer func(mrt *MockRoundTripper, rm RequestMatch) { diff --git a/mock/client_test.go b/mock/client_test.go index 7b5cfd22f10..05cc0a6e176 100644 --- a/mock/client_test.go +++ b/mock/client_test.go @@ -41,7 +41,7 @@ func TestMockClient(t *testing.T) { orgs, _, err := c.Organizations.List( ctx, - *user.Name, + *(user.Name), nil, ) From bb74f7b1a3d4722a1c4edc4ccb6e7e5bbf1768ee Mon Sep 17 00:00:00 2001 From: Miguel Elias dos Santos Date: Tue, 13 Jul 2021 22:21:35 +1000 Subject: [PATCH 4/4] Fix typo on mock/client.go and mock/client_test.go --- mock/client.go | 2 +- mock/client_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mock/client.go b/mock/client.go index 0a869c51aa3..28ca890450c 100644 --- a/mock/client.go +++ b/mock/client.go @@ -135,7 +135,7 @@ func NewMockHttpClient(options ...MockHttpClientOption) *http.Client { } } -func MustMarshall(v interface{}) []byte { +func MustMarshal(v interface{}) []byte { b, err := json.Marshal(v) if err == nil { diff --git a/mock/client_test.go b/mock/client_test.go index 05cc0a6e176..093a552dac4 100644 --- a/mock/client_test.go +++ b/mock/client_test.go @@ -13,13 +13,13 @@ func TestMockClient(t *testing.T) { mockedHttpClient := NewMockHttpClient( WithRequestMatch( RequestMatchUsersGet, - MustMarshall(github.User{ + MustMarshal(github.User{ Name: github.String("foobar"), }), ), WithRequestMatch( RequestMatchOrganizationsList, - MustMarshall([]github.Organization{ + MustMarshal([]github.Organization{ { Name: github.String("foobar123thisorgwasmocked"), },