Skip to content

Commit bea6e56

Browse files
committed
Tests and examples
1 parent d7086e6 commit bea6e56

File tree

8 files changed

+304
-34
lines changed

8 files changed

+304
-34
lines changed

parrot/.changeset/v0.2.0.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ BenchmarkRegisterRoute-14 3647503 313.8 ns/op
2727
BenchmarkRouteResponse-14 19143 62011 ns/op
2828
BenchmarkSave-14 5244 218697 ns/op
2929
BenchmarkLoad-14 1101 1049399 ns/op
30-
```
30+
```

parrot/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ A simple, high-performing mockserver that can dynamically build new routes with
44

55
## Features
66

7-
* Simplistic and fast design
8-
* Run within your Go code, through a small binary, or in a minimal Docker container
9-
* Easily record all incoming requests to the server to programmatically react to
7+
* Run as an imported package, through a small binary, or in a minimal Docker container
8+
* Record all incoming requests to the server and programmatically react
9+
* Match wildcard routes and methods
1010

1111
## Use
1212

parrot/cage.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,7 @@ func (cl *cageLevel) route(routeSegment, routeMethod string) (route *Route, foun
197197
if route, found = cl.routes[routeSegment][routeMethod]; found {
198198
return route, true, nil
199199
}
200-
}
201-
if _, ok := cl.wildCardRoutes[routeSegment]; ok {
202-
if route, found = cl.wildCardRoutes[routeSegment][MethodAny]; found {
200+
if route, found = cl.routes[routeSegment][MethodAny]; found { // Fallthrough to any method if it's designed
203201
return route, true, nil
204202
}
205203
}

parrot/cage_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"github.com/stretchr/testify/require"
88
)
99

10-
func TestCageNewRoutes(t *testing.T) {
10+
func TestCage(t *testing.T) {
1111
t.Parallel()
1212

1313
testCases := []struct {

parrot/examples_test.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,107 @@ func ExampleServer_Register_internal() {
7070
// 0
7171
}
7272

73+
func ExampleServer_Register_wildcards() {
74+
// Create a new parrot instance with no logging and a custom save file
75+
saveFile := "register_example.json"
76+
p, err := parrot.Wake(parrot.WithLogLevel(zerolog.NoLevel), parrot.WithSaveFile(saveFile))
77+
if err != nil {
78+
panic(err)
79+
}
80+
defer func() { // Cleanup the parrot instance
81+
err = p.Shutdown(context.Background()) // Gracefully shutdown the parrot instance
82+
if err != nil {
83+
panic(err)
84+
}
85+
p.WaitShutdown() // Wait for the parrot instance to shutdown. Usually unnecessary, but we want to clean up the save file
86+
os.Remove(saveFile) // Cleanup the save file for the example
87+
}()
88+
89+
// You can use the MethodAny constant to match any HTTP method
90+
anyMethodRoute := &parrot.Route{
91+
Method: parrot.MethodAny,
92+
Path: "/any-method",
93+
RawResponseBody: "Any Method",
94+
ResponseStatusCode: http.StatusOK,
95+
}
96+
97+
err = p.Register(anyMethodRoute)
98+
if err != nil {
99+
panic(err)
100+
}
101+
resp, err := p.Call(http.MethodGet, "/any-method")
102+
if err != nil {
103+
panic(err)
104+
}
105+
fmt.Println(resp.Request.Method, string(resp.Body()))
106+
resp, err = p.Call(http.MethodPost, "/any-method")
107+
if err != nil {
108+
panic(err)
109+
}
110+
fmt.Println(resp.Request.Method, string(resp.Body()))
111+
112+
// A * in the path will match any characters in the path
113+
basicWildCard := &parrot.Route{
114+
Method: parrot.MethodAny,
115+
Path: "/wildcard/*",
116+
RawResponseBody: "Basic Wildcard",
117+
ResponseStatusCode: http.StatusOK,
118+
}
119+
120+
err = p.Register(basicWildCard)
121+
if err != nil {
122+
panic(err)
123+
}
124+
resp, err = p.Call(http.MethodGet, "/wildcard/anything")
125+
if err != nil {
126+
panic(err)
127+
}
128+
fmt.Println(resp.Request.RawRequest.URL.Path, string(resp.Body()))
129+
130+
// Wild cards can be nested
131+
nestedWildCardRoute := &parrot.Route{
132+
Method: parrot.MethodAny,
133+
Path: "/wildcard/*/nested/*",
134+
RawResponseBody: "Nested Wildcard",
135+
ResponseStatusCode: http.StatusOK,
136+
}
137+
138+
err = p.Register(nestedWildCardRoute)
139+
if err != nil {
140+
panic(err)
141+
}
142+
resp, err = p.Call(http.MethodGet, "/wildcard/anything/nested/else")
143+
if err != nil {
144+
panic(err)
145+
}
146+
fmt.Println(resp.Request.RawRequest.URL.Path, string(resp.Body()))
147+
148+
// Wild cards can also be partials
149+
partialWildCardRoute := &parrot.Route{
150+
Method: parrot.MethodAny,
151+
Path: "/partial*/wildcard",
152+
RawResponseBody: "Partial Wildcard",
153+
ResponseStatusCode: http.StatusOK,
154+
}
155+
156+
err = p.Register(partialWildCardRoute)
157+
if err != nil {
158+
panic(err)
159+
}
160+
resp, err = p.Call(http.MethodGet, "/partial_anything/wildcard")
161+
if err != nil {
162+
panic(err)
163+
}
164+
fmt.Println(resp.Request.RawRequest.URL.Path, string(resp.Body()))
165+
166+
// Output:
167+
// GET Any Method
168+
// POST Any Method
169+
// /wildcard/anything Basic Wildcard
170+
// /wildcard/anything/nested/else Nested Wildcard
171+
// /partial_anything/wildcard Partial Wildcard
172+
}
173+
73174
func ExampleServer_Register_external() {
74175
var (
75176
saveFile = "route_example.json"

parrot/parrot.go

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"net/url"
1212
"os"
1313
"path/filepath"
14-
"regexp"
1514
"strconv"
1615
"strings"
1716
"sync"
@@ -322,8 +321,8 @@ func (p *Server) Register(route *Route) error {
322321
if route == nil {
323322
return ErrNilRoute
324323
}
325-
if !isValidPath(route.Path) {
326-
return newDynamicError(ErrInvalidPath, fmt.Sprintf("'%s'", route.Path))
324+
if err := checkPath(route.Path); err != nil {
325+
return newDynamicError(ErrInvalidPath, err.Error())
327326
}
328327
if route.Method == "" {
329328
return ErrNoMethod
@@ -400,6 +399,10 @@ func (p *Server) Delete(route *Route) error {
400399
}
401400

402401
// Call makes a request to the parrot server
402+
// The method is the HTTP method to use (GET, POST, PUT, DELETE, etc.)
403+
// The path is the URL path to call
404+
// The response is returned as a resty.Response
405+
// Errors are returned if the server is shut down or if the request fails, not if the response is an error
403406
func (p *Server) Call(method, path string) (*resty.Response, error) {
404407
if p.shutDown {
405408
return nil, ErrServerShutdown
@@ -491,7 +494,7 @@ func (p *Server) dynamicHandler(w http.ResponseWriter, r *http.Request) {
491494

492495
route, err := p.cage.getRoute(r.URL.Path, r.Method)
493496
if err != nil {
494-
if errors.Is(err, ErrRouteNotFound) {
497+
if errors.Is(err, ErrRouteNotFound) || errors.Is(err, ErrCageNotFound) {
495498
http.Error(w, "Route not found", http.StatusNotFound)
496499
dynamicLogger.Debug().Msg("Route not found")
497500
return
@@ -754,36 +757,58 @@ func (p *Server) loggingMiddleware(next http.Handler) http.Handler {
754757
return h(accessHandler(next))
755758
}
756759

757-
var validPathRegex = regexp.MustCompile(`^\/[a-zA-Z0-9\-._~%!$&'()+,;=:@\/]`)
758-
759-
func isValidPath(path string) bool {
760+
func checkPath(path string) error {
760761
switch path {
761762
case "", "/", "//", healthRoute, recordRoute, routesRoute, "/..":
762-
return false
763+
return fmt.Errorf("cannot match special paths: '%s'", path)
763764
}
764765
if strings.Contains(path, "/..") {
765-
return false
766+
return fmt.Errorf("cannot match parent directory traversal: '%s'", path)
766767
}
767768
if strings.Contains(path, "/.") {
768-
return false
769+
return fmt.Errorf("cannot match hidden files: '%s'", path)
769770
}
770771
if strings.Contains(path, "//") {
771-
return false
772+
return fmt.Errorf("cannot match double slashes: '%s'", path)
772773
}
773774
if !strings.HasPrefix(path, "/") {
774-
return false
775+
return fmt.Errorf("path must start with a forward slash: '%s'", path)
775776
}
776777
if strings.HasSuffix(path, "/") {
777-
return false
778+
return fmt.Errorf("path cannot end with a forward slash: '%s'", path)
778779
}
779780
if strings.HasPrefix(path, recordRoute) {
780-
return false
781+
return fmt.Errorf("cannot match record route: '%s'", path)
781782
}
782783
if strings.HasPrefix(path, healthRoute) {
783-
return false
784+
return fmt.Errorf("cannot match health route: '%s'", path)
784785
}
785786
if strings.HasPrefix(path, routesRoute) {
786-
return false
787+
return fmt.Errorf("cannot match routes route: '%s'", path)
788+
}
789+
match, err := filepath.Match(path, healthRoute)
790+
if err != nil {
791+
return fmt.Errorf("failed to match: '%s'", path)
792+
}
793+
if match {
794+
return fmt.Errorf("cannot match health route: '%s'", path)
795+
}
796+
797+
match, err = filepath.Match(path, recordRoute)
798+
if err != nil {
799+
return fmt.Errorf("failed to match: '%s'", path)
787800
}
788-
return validPathRegex.MatchString(path)
801+
if match {
802+
return fmt.Errorf("cannot match record route: '%s'", path)
803+
}
804+
805+
match, err = filepath.Match(path, routesRoute)
806+
if err != nil {
807+
return fmt.Errorf("failed to match: '%s'", path)
808+
}
809+
if match {
810+
return fmt.Errorf("cannot match routes route: '%s'", path)
811+
}
812+
813+
return nil
789814
}

parrot/parrot_benchmark_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ func BenchmarkRegisterRoute(b *testing.B) {
4141
b.StopTimer()
4242
}
4343

44+
func BenchmarkRegisterWildCardRoute(b *testing.B) {
45+
// TODO: Implement
46+
}
47+
4448
func BenchmarkRouteResponse(b *testing.B) {
4549
saveFile := b.Name() + ".json"
4650
p, err := Wake(WithLogLevel(testLogLevel), WithSaveFile(saveFile))
@@ -72,6 +76,10 @@ func BenchmarkRouteResponse(b *testing.B) {
7276
b.StopTimer()
7377
}
7478

79+
func BenchmarkWildCardRouteResponse(b *testing.B) {
80+
81+
}
82+
7583
func BenchmarkSave(b *testing.B) {
7684
var (
7785
routes = []*Route{}

0 commit comments

Comments
 (0)