Skip to content

Commit b8d60fa

Browse files
committed
Adds methodAny and tests
1 parent faf9a06 commit b8d60fa

File tree

7 files changed

+123
-47
lines changed

7 files changed

+123
-47
lines changed

parrot/Makefile

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ test_race:
2020
set -euo pipefail
2121
go test $(TEST_ARGS) -json -timeout $(TEST_TIMEOUT) -cover -count=1 -race -coverprofile cover.out -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
2222

23+
2324
.PHONY: test_unit
2425
test_unit:
2526
go test $(TEST_ARGS) -timeout $(TEST_TIMEOUT) -coverprofile cover.out ./...
@@ -28,10 +29,11 @@ test_unit:
2829
bench:
2930
go test $(TEST_ARGS) -bench=. -run=^$$ ./...
3031

32+
.PHONY: fuzz_tests
33+
fuzz:
34+
go test -list="Fuzz" ./...
35+
3136
.PHONY: build
3237
build:
33-
go build -o ./parrot ./cmd
34-
35-
.PHONY: goreleaser
36-
goreleaser:
37-
cd .. && goreleaser release --snapshot --clean -f ./parrot/.goreleaser.yaml
38+
cd .. && goreleaser release --snapshot --clean -f ./parrot/.goreleaser.yaml
39+

parrot/errors.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ import (
77

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

1718
ErrNoRecorderURL = errors.New("no recorder URL specified")
1819
ErrInvalidRecorderURL = errors.New("invalid recorder URL")

parrot/examples_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
1313
)
1414

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

72-
func ExampleServer_Register_external() {
72+
func ExampleServer_external() {
7373
var (
7474
saveFile = "route_example.json"
7575
port = 9090

parrot/parrot.go

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ const (
2828
HealthRoute = "/health"
2929
RoutesRoute = "/routes"
3030
RecorderRoute = "/recorder"
31+
32+
// MethodAny is a wildcard for any HTTP method
33+
MethodAny = "ANY"
3134
)
3235

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

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

212-
if route.Handler != nil {
213-
route.Handler(w, r)
214-
return
215-
}
216-
217212
if route.RawResponseBody != "" {
218213
w.Header().Set("Content-Type", "text/plain")
219214
w.WriteHeader(route.ResponseStatusCode)
@@ -317,15 +312,12 @@ func (p *Server) Register(route *Route) error {
317312
if !isValidPath(route.Path) {
318313
return newDynamicError(ErrInvalidPath, fmt.Sprintf("'%s'", route.Path))
319314
}
320-
if route.Method == "" {
321-
return ErrNoMethod
315+
if !isValidMethod(route.Method) {
316+
return newDynamicError(ErrInvalidMethod, fmt.Sprintf("'%s'", route.Method))
322317
}
323-
if route.Handler == nil && route.ResponseBody == nil && route.RawResponseBody == "" {
318+
if route.ResponseBody == nil && route.RawResponseBody == "" {
324319
return ErrNoResponse
325320
}
326-
if route.Handler != nil && (route.ResponseBody != nil || route.RawResponseBody != "") {
327-
return newDynamicError(ErrOnlyOneResponse, "handler and another response type provided")
328-
}
329321
if route.ResponseBody != nil && route.RawResponseBody != "" {
330322
return ErrOnlyOneResponse
331323
}
@@ -334,8 +326,19 @@ func (p *Server) Register(route *Route) error {
334326
return newDynamicError(ErrResponseMarshal, err.Error())
335327
}
336328
}
329+
numWildcards := strings.Count(route.Path, "*")
330+
if 1 < numWildcards {
331+
return newDynamicError(ErrWildcardPath, fmt.Sprintf("more than 1 wildcard '%s'", route.Path))
332+
}
333+
if numWildcards == 1 && !strings.HasSuffix(route.Path, "*") {
334+
return newDynamicError(ErrWildcardPath, fmt.Sprintf("wildcard not at end '%s'", route.Path))
335+
}
337336

338-
p.router.MethodFunc(route.Method, route.Path, routeRecordingMiddleware(p, p.routeCallHandler(route)))
337+
if route.Method == MethodAny {
338+
p.router.Handle(route.Path, routeRecordingMiddleware(p, p.routeCallHandler(route)))
339+
} else {
340+
p.router.MethodFunc(route.Method, route.Path, routeRecordingMiddleware(p, p.routeCallHandler(route)))
341+
}
339342

340343
p.routesMu.Lock()
341344
defer p.routesMu.Unlock()
@@ -496,6 +499,9 @@ func (p *Server) Call(method, path string) (*resty.Response, error) {
496499
if p.shutDown.Load() {
497500
return nil, ErrServerShutdown
498501
}
502+
if !isValidMethod(method) {
503+
return nil, newDynamicError(ErrInvalidMethod, fmt.Sprintf("'%s'", method))
504+
}
499505
return p.client.R().Execute(method, "http://"+filepath.Join(p.Address(), path))
500506
}
501507

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

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

669+
// isValidPath checks if the path is a valid URL path
663670
func isValidPath(path string) bool {
664671
if path == "" || path == "/" {
665672
return false
@@ -681,3 +688,19 @@ func isValidPath(path string) bool {
681688
}
682689
return pathRegex.MatchString(path)
683690
}
691+
692+
// isValidMethod checks if the method is a valid HTTP method, in loose terms
693+
func isValidMethod(method string) bool {
694+
switch method {
695+
case
696+
http.MethodGet,
697+
http.MethodPost,
698+
http.MethodPut,
699+
http.MethodPatch,
700+
http.MethodDelete,
701+
http.MethodOptions,
702+
MethodAny:
703+
return true
704+
}
705+
return false
706+
}

parrot/parrot_fuzz_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package parrot
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func FuzzRegisterPath(f *testing.F) {
12+
p := newParrot(f)
13+
14+
baseRoute := Route{
15+
Method: http.MethodGet,
16+
ResponseStatusCode: http.StatusOK,
17+
RawResponseBody: "Squawk",
18+
}
19+
f.Add("/foo")
20+
f.Add("/foo/bar")
21+
f.Add("/*")
22+
f.Add("/foo/*")
23+
24+
f.Fuzz(func(t *testing.T, path string) {
25+
route := baseRoute
26+
route.Path = path
27+
28+
_ = p.Register(&route) // We just don't want panics
29+
})
30+
}
31+
32+
func FuzzMethodAny(f *testing.F) {
33+
p := newParrot(f)
34+
35+
route := &Route{
36+
Method: MethodAny,
37+
Path: "/any",
38+
ResponseStatusCode: http.StatusOK,
39+
RawResponseBody: "Squawk",
40+
}
41+
42+
err := p.Register(route)
43+
require.NoError(f, err)
44+
45+
f.Add(http.MethodGet)
46+
f.Add(http.MethodPost)
47+
f.Add(http.MethodPut)
48+
f.Add(http.MethodPatch)
49+
f.Add(http.MethodDelete)
50+
f.Add(http.MethodOptions)
51+
52+
f.Fuzz(func(t *testing.T, method string) {
53+
if !isValidMethod(method) {
54+
t.Skip("invalid method")
55+
}
56+
resp, err := p.Call(method, route.Path)
57+
require.NoError(t, err)
58+
59+
require.Equal(t, http.StatusOK, resp.StatusCode(), fmt.Sprintf("bad response code with method: '%s'", method))
60+
require.Equal(t, "Squawk", string(resp.Body()), fmt.Sprintf("bad response body with method: '%s'", method))
61+
})
62+
}

parrot/parrot_test.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ func TestBadRegisterRoute(t *testing.T) {
306306
},
307307
{
308308
name: "no method",
309-
err: ErrNoMethod,
309+
err: ErrInvalidMethod,
310310
route: &Route{
311311
Path: "/hello",
312312
RawResponseBody: "Squawk",
@@ -372,20 +372,6 @@ func TestBadRegisterRoute(t *testing.T) {
372372
ResponseStatusCode: http.StatusOK,
373373
},
374374
},
375-
{
376-
name: "too many responses",
377-
err: ErrOnlyOneResponse,
378-
route: &Route{
379-
Method: http.MethodGet,
380-
Path: "/hello",
381-
ResponseBody: map[string]any{"message": "Squawk"},
382-
Handler: func(w http.ResponseWriter, r *http.Request) {
383-
w.WriteHeader(http.StatusOK)
384-
_, _ = w.Write([]byte("Squawk"))
385-
},
386-
ResponseStatusCode: http.StatusOK,
387-
},
388-
},
389375
{
390376
name: "bad JSON",
391377
err: ErrResponseMarshal,
@@ -586,16 +572,16 @@ func TestJSONLogger(t *testing.T) {
586572
require.Contains(t, string(logs), fmt.Sprintf(`"Route ID":"%s"`, route.ID()), "expected log file to contain route call in JSON format")
587573
}
588574

589-
func newParrot(t *testing.T) *Server {
590-
t.Helper()
575+
func newParrot(tb testing.TB) *Server {
576+
tb.Helper()
591577

592-
logFileName := t.Name() + ".log"
593-
saveFileName := t.Name() + ".json"
578+
logFileName := tb.Name() + ".log"
579+
saveFileName := tb.Name() + ".json"
594580
p, err := Wake(WithSaveFile(saveFileName), WithLogFile(logFileName), WithLogLevel(testLogLevel))
595-
require.NoError(t, err, "error waking parrot")
596-
t.Cleanup(func() {
581+
require.NoError(tb, err, "error waking parrot")
582+
tb.Cleanup(func() {
597583
err := p.Shutdown(context.Background())
598-
assert.NoError(t, err, "error shutting down parrot")
584+
assert.NoError(tb, err, "error shutting down parrot")
599585
p.WaitShutdown() // Wait for shutdown to complete and file to be written
600586
os.Remove(saveFileName)
601587
os.Remove(logFileName)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("0")

0 commit comments

Comments
 (0)