Skip to content

Commit

Permalink
Adds methodAny and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
kalverra committed Jan 31, 2025
1 parent faf9a06 commit b8d60fa
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 47 deletions.
12 changes: 7 additions & 5 deletions parrot/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ test_race:
set -euo pipefail
go test $(TEST_ARGS) -json -timeout $(TEST_TIMEOUT) -cover -count=1 -race -coverprofile cover.out -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt


.PHONY: test_unit
test_unit:
go test $(TEST_ARGS) -timeout $(TEST_TIMEOUT) -coverprofile cover.out ./...
Expand All @@ -28,10 +29,11 @@ test_unit:
bench:
go test $(TEST_ARGS) -bench=. -run=^$$ ./...

.PHONY: fuzz_tests
fuzz:
go test -list="Fuzz" ./...

.PHONY: build
build:
go build -o ./parrot ./cmd

.PHONY: goreleaser
goreleaser:
cd .. && goreleaser release --snapshot --clean -f ./parrot/.goreleaser.yaml
cd .. && goreleaser release --snapshot --clean -f ./parrot/.goreleaser.yaml

3 changes: 2 additions & 1 deletion parrot/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import (

var (
ErrNilRoute = errors.New("route is nil")
ErrNoMethod = errors.New("no method specified")
ErrInvalidPath = errors.New("invalid path")
ErrInvalidMethod = errors.New("invalid method")
ErrNoResponse = errors.New("route must have a handler or some response")
ErrOnlyOneResponse = errors.New("route can only have one response type")
ErrResponseMarshal = errors.New("unable to marshal response body to JSON")
ErrRouteNotFound = errors.New("route not found")
ErrWildcardPath = fmt.Errorf("path can only contain one wildcard '*' and it must be the final value")

ErrNoRecorderURL = errors.New("no recorder URL specified")
ErrInvalidRecorderURL = errors.New("invalid recorder URL")
Expand Down
4 changes: 2 additions & 2 deletions parrot/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
)

func ExampleServer_Register_internal() {
func ExampleServer_internal() {
// Create a new parrot instance with no logging and a custom save file
saveFile := "register_example.json"
p, err := parrot.Wake(parrot.WithLogLevel(zerolog.NoLevel), parrot.WithSaveFile(saveFile))
Expand Down Expand Up @@ -69,7 +69,7 @@ func ExampleServer_Register_internal() {
// 0
}

func ExampleServer_Register_external() {
func ExampleServer_external() {
var (
saveFile = "route_example.json"
port = 9090
Expand Down
57 changes: 40 additions & 17 deletions parrot/parrot.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const (
HealthRoute = "/health"
RoutesRoute = "/routes"
RecorderRoute = "/recorder"

// MethodAny is a wildcard for any HTTP method
MethodAny = "ANY"
)

// Route holds information about the mock route configuration
Expand All @@ -36,9 +39,6 @@ type Route struct {
Method string `json:"Method"`
// Path is the URL path to match
Path string `json:"Path"`
// Handler is the dynamic handler function to use when called
// Can only be set upon creation of the server
Handler http.HandlerFunc `json:"-"`
// RawResponseBody is the static, raw string response to return when called
RawResponseBody string `json:"raw_response_body"`
// ResponseBody will be marshalled to JSON and returned when called
Expand Down Expand Up @@ -187,7 +187,7 @@ func (p *Server) run(listener net.Listener) {
p.log.Error().Err(err).Msg("Failed to save routes")
}
if err := p.logFile.Close(); err != nil {
p.log.Error().Err(err).Msg("Failed to close log file")
fmt.Println("ERROR: Failed to close log file:", err)
}
p.shutDownOnce.Do(func() {
close(p.shutDownChan)
Expand All @@ -197,7 +197,7 @@ func (p *Server) run(listener net.Listener) {
p.log.Info().Str("Address", p.address).Msg("Parrot awake and ready to squawk")
p.log.Debug().Str("Save File", p.saveFileName).Str("Log File", p.logFileName).Msg("Configuration")
if err := p.server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
p.log.Fatal().Err(err).Msg("Error while running server")
fmt.Println("ERROR: Failed to start server:", err)
}
}

Expand All @@ -209,11 +209,6 @@ func (p *Server) routeCallHandler(route *Route) http.HandlerFunc {
return c.Str("Route ID", route.ID())
})

if route.Handler != nil {
route.Handler(w, r)
return
}

if route.RawResponseBody != "" {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(route.ResponseStatusCode)
Expand Down Expand Up @@ -317,15 +312,12 @@ func (p *Server) Register(route *Route) error {
if !isValidPath(route.Path) {
return newDynamicError(ErrInvalidPath, fmt.Sprintf("'%s'", route.Path))
}
if route.Method == "" {
return ErrNoMethod
if !isValidMethod(route.Method) {
return newDynamicError(ErrInvalidMethod, fmt.Sprintf("'%s'", route.Method))
}
if route.Handler == nil && route.ResponseBody == nil && route.RawResponseBody == "" {
if route.ResponseBody == nil && route.RawResponseBody == "" {
return ErrNoResponse
}
if route.Handler != nil && (route.ResponseBody != nil || route.RawResponseBody != "") {
return newDynamicError(ErrOnlyOneResponse, "handler and another response type provided")
}
if route.ResponseBody != nil && route.RawResponseBody != "" {
return ErrOnlyOneResponse
}
Expand All @@ -334,8 +326,19 @@ func (p *Server) Register(route *Route) error {
return newDynamicError(ErrResponseMarshal, err.Error())
}
}
numWildcards := strings.Count(route.Path, "*")
if 1 < numWildcards {
return newDynamicError(ErrWildcardPath, fmt.Sprintf("more than 1 wildcard '%s'", route.Path))
}
if numWildcards == 1 && !strings.HasSuffix(route.Path, "*") {
return newDynamicError(ErrWildcardPath, fmt.Sprintf("wildcard not at end '%s'", route.Path))
}

p.router.MethodFunc(route.Method, route.Path, routeRecordingMiddleware(p, p.routeCallHandler(route)))
if route.Method == MethodAny {
p.router.Handle(route.Path, routeRecordingMiddleware(p, p.routeCallHandler(route)))
} else {
p.router.MethodFunc(route.Method, route.Path, routeRecordingMiddleware(p, p.routeCallHandler(route)))
}

p.routesMu.Lock()
defer p.routesMu.Unlock()
Expand Down Expand Up @@ -496,6 +499,9 @@ func (p *Server) Call(method, path string) (*resty.Response, error) {
if p.shutDown.Load() {
return nil, ErrServerShutdown
}
if !isValidMethod(method) {
return nil, newDynamicError(ErrInvalidMethod, fmt.Sprintf("'%s'", method))
}
return p.client.R().Execute(method, "http://"+filepath.Join(p.Address(), path))
}

Expand Down Expand Up @@ -660,6 +666,7 @@ func (p *Server) loggingMiddleware(next http.Handler) http.Handler {

var pathRegex = regexp.MustCompile(`^\/[a-zA-Z0-9\-._~%!$&'()*+,;=:@\/]*$`)

// isValidPath checks if the path is a valid URL path
func isValidPath(path string) bool {
if path == "" || path == "/" {
return false
Expand All @@ -681,3 +688,19 @@ func isValidPath(path string) bool {
}
return pathRegex.MatchString(path)
}

// isValidMethod checks if the method is a valid HTTP method, in loose terms
func isValidMethod(method string) bool {
switch method {
case
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
MethodAny:
return true
}
return false
}
62 changes: 62 additions & 0 deletions parrot/parrot_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package parrot

import (
"fmt"
"net/http"
"testing"

"github.com/stretchr/testify/require"
)

func FuzzRegisterPath(f *testing.F) {
p := newParrot(f)

baseRoute := Route{
Method: http.MethodGet,
ResponseStatusCode: http.StatusOK,
RawResponseBody: "Squawk",
}
f.Add("/foo")
f.Add("/foo/bar")
f.Add("/*")
f.Add("/foo/*")

f.Fuzz(func(t *testing.T, path string) {
route := baseRoute
route.Path = path

_ = p.Register(&route) // We just don't want panics
})
}

func FuzzMethodAny(f *testing.F) {
p := newParrot(f)

route := &Route{
Method: MethodAny,
Path: "/any",
ResponseStatusCode: http.StatusOK,
RawResponseBody: "Squawk",
}

err := p.Register(route)
require.NoError(f, err)

f.Add(http.MethodGet)
f.Add(http.MethodPost)
f.Add(http.MethodPut)
f.Add(http.MethodPatch)
f.Add(http.MethodDelete)
f.Add(http.MethodOptions)

f.Fuzz(func(t *testing.T, method string) {
if !isValidMethod(method) {
t.Skip("invalid method")
}
resp, err := p.Call(method, route.Path)
require.NoError(t, err)

require.Equal(t, http.StatusOK, resp.StatusCode(), fmt.Sprintf("bad response code with method: '%s'", method))
require.Equal(t, "Squawk", string(resp.Body()), fmt.Sprintf("bad response body with method: '%s'", method))
})
}
30 changes: 8 additions & 22 deletions parrot/parrot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ func TestBadRegisterRoute(t *testing.T) {
},
{
name: "no method",
err: ErrNoMethod,
err: ErrInvalidMethod,
route: &Route{
Path: "/hello",
RawResponseBody: "Squawk",
Expand Down Expand Up @@ -372,20 +372,6 @@ func TestBadRegisterRoute(t *testing.T) {
ResponseStatusCode: http.StatusOK,
},
},
{
name: "too many responses",
err: ErrOnlyOneResponse,
route: &Route{
Method: http.MethodGet,
Path: "/hello",
ResponseBody: map[string]any{"message": "Squawk"},
Handler: func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Squawk"))
},
ResponseStatusCode: http.StatusOK,
},
},
{
name: "bad JSON",
err: ErrResponseMarshal,
Expand Down Expand Up @@ -586,16 +572,16 @@ func TestJSONLogger(t *testing.T) {
require.Contains(t, string(logs), fmt.Sprintf(`"Route ID":"%s"`, route.ID()), "expected log file to contain route call in JSON format")
}

func newParrot(t *testing.T) *Server {
t.Helper()
func newParrot(tb testing.TB) *Server {
tb.Helper()

logFileName := t.Name() + ".log"
saveFileName := t.Name() + ".json"
logFileName := tb.Name() + ".log"
saveFileName := tb.Name() + ".json"
p, err := Wake(WithSaveFile(saveFileName), WithLogFile(logFileName), WithLogLevel(testLogLevel))
require.NoError(t, err, "error waking parrot")
t.Cleanup(func() {
require.NoError(tb, err, "error waking parrot")
tb.Cleanup(func() {
err := p.Shutdown(context.Background())
assert.NoError(t, err, "error shutting down parrot")
assert.NoError(tb, err, "error shutting down parrot")
p.WaitShutdown() // Wait for shutdown to complete and file to be written
os.Remove(saveFileName)
os.Remove(logFileName)
Expand Down
2 changes: 2 additions & 0 deletions parrot/testdata/fuzz/FuzzMethodAny/771e938e4458e983
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
go test fuzz v1
string("0")

0 comments on commit b8d60fa

Please sign in to comment.