Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: general clean up #5

Merged
merged 1 commit into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Package httpregistry provides multiple utilities that can be used to simplify the creation of /net/http/httptest
mock servers.
That package allows the creation of http servers that can be used to respond to actual http calls in tests.
This package aims at providing a nicer interface that should cover the most standard cases and attempts to hide away a layer of boilerplate.
For example it is normal to write test code like this

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/users" {
w.WriteHeader(http.StatusOK)
return
}

w.WriteHeader(http.StatusInternalServerError)
}))

with this package this can be simplified to

registry := NewRegistry()
registry.AddSimpleRequest("/users", http.MethodGet)
ts := registry.GetServer()

Similarly this package tries to help with the harder task to test if a POST requests
*/
package httpregistry
40 changes: 20 additions & 20 deletions match.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import (
)

var (
ErrNoNextResponseFound = errors.New("it was not possible to found a next response")
errNoNextResponseFound = errors.New("it was not possible to found a next response")
)

// A Match is used to connect a Request to one or multiple possible Response(s) so that when the request happens the mock server
// A match is used to connect a Request to one or multiple possible Response(s) so that when the request happens the mock server
// returns the desired response.
type Match interface {
type match interface {
// Request returns the request that triggers the match
Request() Request
// Next response returns the next response associated with the match and records which request triggered the match.
Expand All @@ -23,17 +23,17 @@ type Match interface {
Matches() []*http.Request
}

// A FixedResponseMatch is a match that returns a fixed Response each time a predefined Request happens
type FixedResponseMatch struct {
// A fixedResponseMatch is a match that returns a fixed Response each time a predefined Request happens
type fixedResponseMatch struct {
request Request
response Response
numberOfCalls int
matches []*http.Request
}

// NewFixedResponseMatch creates a new FixedResponseMatch
func NewFixedResponseMatch(request Request, response Response) *FixedResponseMatch {
return &FixedResponseMatch{
// newFixedResponseMatch creates a new FixedResponseMatch
func newFixedResponseMatch(request Request, response Response) *fixedResponseMatch {
return &fixedResponseMatch{
request: request,
response: response,
numberOfCalls: 0,
Expand All @@ -42,41 +42,41 @@ func NewFixedResponseMatch(request Request, response Response) *FixedResponseMat
}

// Request returns the request that triggers the match
func (m *FixedResponseMatch) Request() Request {
func (m *fixedResponseMatch) Request() Request {
return m.request
}

// Next response returns the next response associated with the match and records which request triggered the match.
// It never raises an error
func (m *FixedResponseMatch) NextResponse(req *http.Request) (Response, error) {
func (m *fixedResponseMatch) NextResponse(req *http.Request) (Response, error) {
m.matches = append(m.matches, cloneHttpRequest(req))

return m.response, nil
}

// Matches returns the list of http.Request that matched with this Match
func (m *FixedResponseMatch) Matches() []*http.Request {
func (m *fixedResponseMatch) Matches() []*http.Request {
return m.matches
}

// NumberOfCalls returns the number of times the match was fulfilled
func (m *FixedResponseMatch) NumberOfCalls() int {
func (m *fixedResponseMatch) NumberOfCalls() int {
return len(m.matches)
}

// A FixedResponseMatch is a match that returns a different Response each time a predefined Request happens
//
// Important: the list of responses gets consumed by the server. Do not reuse this structure, create a new one
type MultipleResponsesMatch struct {
type multipleResponsesMatch struct {
request Request
responses Responses
numberOfCalls int
matches []*http.Request
}

// NewFixedResponseMatch creates a new FixedResponseMatch
func NewMultipleResponsesMatch(request Request, responses Responses) *MultipleResponsesMatch {
return &MultipleResponsesMatch{
func newMultipleResponsesMatch(request Request, responses Responses) *multipleResponsesMatch {
return &multipleResponsesMatch{
request: request,
responses: responses,
numberOfCalls: 0,
Expand All @@ -85,16 +85,16 @@ func NewMultipleResponsesMatch(request Request, responses Responses) *MultipleRe
}

// Request returns the request that triggers the match
func (m *MultipleResponsesMatch) Request() Request {
func (m *multipleResponsesMatch) Request() Request {
return m.request
}

// Next response returns the next response associated with the match.
// If the list of responses is exhausted it will return a ErrNoNextResponseFound error
// It consumes the list associated with the MultipleResponsesMatch
func (m *MultipleResponsesMatch) NextResponse(req *http.Request) (Response, error) {
func (m *multipleResponsesMatch) NextResponse(req *http.Request) (Response, error) {
if len(m.responses) == 0 {
return Response{}, ErrNoNextResponseFound
return Response{}, errNoNextResponseFound
}

m.matches = append(m.matches, cloneHttpRequest(req))
Expand All @@ -106,11 +106,11 @@ func (m *MultipleResponsesMatch) NextResponse(req *http.Request) (Response, erro
}

// Matches returns the list of http.Request that matched with this Match
func (m *MultipleResponsesMatch) Matches() []*http.Request {
func (m *multipleResponsesMatch) Matches() []*http.Request {
return m.matches
}

// NumberOfCalls returns the number of times the match was fulfilled
func (m *MultipleResponsesMatch) NumberOfCalls() int {
func (m *multipleResponsesMatch) NumberOfCalls() int {
return len(m.matches)
}
34 changes: 16 additions & 18 deletions match_test.go
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
package httpregistry_test
package httpregistry

import (
"net/http"

"github.com/dfioravanti/httpregistry"
)

func (s *TestSuite) TestFixedResponseMatchHasExpectedResponse() {
request := httpregistry.NewRequest("/", http.MethodPost)
response := httpregistry.NoContentResponse
request := NewRequest("/", http.MethodPost)
response := NoContentResponse

match := httpregistry.NewFixedResponseMatch(request, response)
match := newFixedResponseMatch(request, response)

s.Equal(request, match.Request())
}

func (s *TestSuite) TestFixedResponseRespondsForever() {
request := httpregistry.NewRequest("/", http.MethodPost)
expectedResponse := httpregistry.NoContentResponse
request := NewRequest("/", http.MethodPost)
expectedResponse := NoContentResponse

match := httpregistry.NewFixedResponseMatch(request, expectedResponse)
match := newFixedResponseMatch(request, expectedResponse)
expectedNumberOfCalls := 1000

for range expectedNumberOfCalls {
Expand All @@ -34,22 +32,22 @@ func (s *TestSuite) TestFixedResponseRespondsForever() {
}

func (s *TestSuite) TestMultipleResponsesHasExpectedResponse() {
request := httpregistry.NewRequest("/", http.MethodPost)
responses := httpregistry.Responses{httpregistry.NoContentResponse}
request := NewRequest("/", http.MethodPost)
responses := Responses{NoContentResponse}

match := httpregistry.NewMultipleResponsesMatch(request, responses)
match := newMultipleResponsesMatch(request, responses)

s.Equal(request, match.Request())
}

func (s *TestSuite) TestMultipleResponsesRespondsTheCorrectNumberOfTimes() {
request := httpregistry.NewRequest("/", http.MethodPost)
request := NewRequest("/", http.MethodPost)

expectedFirstResponse := httpregistry.CreatedResponse
expectedSecondResponse := httpregistry.NoContentResponse
responses := httpregistry.Responses{expectedFirstResponse, expectedSecondResponse}
expectedFirstResponse := CreatedResponse
expectedSecondResponse := NoContentResponse
responses := Responses{expectedFirstResponse, expectedSecondResponse}

match := httpregistry.NewMultipleResponsesMatch(request, responses)
match := newMultipleResponsesMatch(request, responses)

firstResponse, err := match.NextResponse(&http.Request{})
s.NoError(err)
Expand All @@ -60,5 +58,5 @@ func (s *TestSuite) TestMultipleResponsesRespondsTheCorrectNumberOfTimes() {
s.Equal(expectedSecondResponse, secondResponse)

_, err = match.NextResponse(&http.Request{})
s.ErrorIs(err, httpregistry.ErrNoNextResponseFound)
s.ErrorIs(err, errNoNextResponseFound)
}
5 changes: 3 additions & 2 deletions misses.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ type whyMissed string
const (
pathDoesNotMatch = whyMissed("The path does not match")
methodDoesNotMatch = whyMissed("The method does not match")
headersDoNotMatch = whyMissed("The headers do not match")
)

type Miss struct {
type miss struct {
MissedMatch Request
Why whyMissed
}

func (m Miss) String() string {
func (m miss) String() string {
return fmt.Sprintf("%v missed %v", m.MissedMatch, m.Why)
}
109 changes: 74 additions & 35 deletions registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (

// A Registry contains all the Match that were registered and it is designed to allow easy access and manipulation to them
type Registry struct {
matches []Match
misses []Miss
matches []match
misses []miss
}

// NewRegistry creates a new Registry
Expand All @@ -24,25 +24,29 @@ func NewRegistry() *Registry {
func (reg *Registry) AddSimpleRequest(URL string, method string) {
request := NewRequest(URL, method)

reg.matches = append(reg.matches, NewFixedResponseMatch(request, OkResponse))
reg.matches = append(reg.matches, newFixedResponseMatch(request, OkResponse))
}

// AddSimpleRequest is a helper function for the common case of wanting to return a statusCode response when URL is called with a method
func (m *Registry) AddSimpleRequestWithStatusCode(URL string, method string, statusCode int) {
func (reg *Registry) AddSimpleRequestWithStatusCode(URL string, method string, statusCode int) {
request := NewRequest(URL, method)
response := NewResponse(statusCode, nil)

m.matches = append(m.matches, NewFixedResponseMatch(request, response))
reg.matches = append(reg.matches, newFixedResponseMatch(request, response))
}

// AddRequest adds request to the mock server and it returns a 200 response each time that request happens
func (reg *Registry) AddRequest(request Request) {
reg.matches = append(reg.matches, NewFixedResponseMatch(request, OkResponse))
reg.matches = append(reg.matches, newFixedResponseMatch(request, OkResponse))
}

// AddRequest adds request to the mock server and it returns response each time that request happens
func (reg *Registry) AddRequestWithResponse(request Request, response Response) {
reg.matches = append(reg.matches, NewFixedResponseMatch(request, response))
reg.matches = append(reg.matches, newFixedResponseMatch(request, response))
}

func (reg *Registry) AddRequestWithResponses(request Request, responses ...Response) {
reg.matches = append(reg.matches, newMultipleResponsesMatch(request, responses))
}

func (reg *Registry) GetMatchesPerRequest(r Request) []*http.Request {
Expand Down Expand Up @@ -80,51 +84,86 @@ func (reg *Registry) GetMatchesUrlAndMethod(url string, method string) []*http.R
// GetServer returns a httptest.Server designed to match all the requests registered with the Registry
func (reg *Registry) GetServer(t *testing.T) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pathToMatch := r.URL.String()
methodToMatch := r.Method

// We reset the misses since if a previous request matched it is pointless to record that some of the mocks did not match it.
// If said request did not match then the test would have crashed in any case so the information in misses is useless.
reg.misses = []miss{}
for _, possibleMatch := range reg.matches {
requestToMatch := possibleMatch.Request()
if requestToMatch.urlAsRegex.MatchString(pathToMatch) {
response, err := possibleMatch.NextResponse(r)
if err != nil {
if errors.Is(ErrNoNextResponseFound, err) {
t.Errorf("run out of responses when calling: %v %v", requestToMatch.Method, requestToMatch.Url)
}

if !requestToMatch.urlAsRegex.MatchString(r.URL.String()) {
miss := miss{
MissedMatch: requestToMatch,
Why: pathDoesNotMatch,
}
reg.misses = append(reg.misses, miss)

if methodToMatch == r.Method {
for k, v := range response.headers {
w.Header().Add(k, v)
}
w.WriteHeader(response.status)
_, err = w.Write(response.body)
if err != nil {
panic("cannot write body of request")
}
continue
}

if requestToMatch.Method != r.Method {
miss := miss{
MissedMatch: requestToMatch,
Why: methodDoesNotMatch,
}
reg.misses = append(reg.misses, miss)

return
} else {
miss := Miss{
continue
}

headersToMatch := requestToMatch.Headers
headersMatch := true
for headerToMatch, valueToMatch := range headersToMatch {
value := r.Header.Get(headerToMatch)
if value == "" || value != valueToMatch {
headersMatch = false
miss := miss{
MissedMatch: requestToMatch,
Why: methodDoesNotMatch,
Why: headersDoNotMatch,
}
reg.misses = append(reg.misses, miss)

break
}
} else {
miss := Miss{
MissedMatch: requestToMatch,
Why: pathDoesNotMatch,
}

if !headersMatch {
continue
}

response, err := possibleMatch.NextResponse(r)
if err != nil {
if errors.Is(errNoNextResponseFound, err) {
t.Errorf("run out of responses when calling: %v %v", requestToMatch.Method, requestToMatch.Url)
}
reg.misses = append(reg.misses, miss)
}

for k, v := range response.headers {
w.Header().Add(k, v)
}
w.WriteHeader(response.status)
_, err = w.Write(response.body)
if err != nil {
panic("cannot write body of request")
}
return
}

res, err := httputil.DumpRequest(r, true)
if err != nil {
t.Errorf("impossible to dump http request with error %v", err)
}

t.Errorf("no registered request matched %v, you can use .Why() to get an explanation of why", res)
t.Errorf("no registered request matched %v\n you can use .Why() to get an explanation of why", string(res))
}))
}

// Why returns a string that contains all the reasons why the request submitted to the registry failed to match with the registered requests.
// The envision use of this function is just as a helper when debugging the tests,
// most of the time it might not be obvious if there is a typo or a small error.
func (reg *Registry) Why() string {
output := ""
for _, miss := range reg.misses {
output += miss.String() + "\n"
}
return output
}
Loading
Loading