Skip to content

Commit

Permalink
Tests and examples
Browse files Browse the repository at this point in the history
  • Loading branch information
kalverra committed Jan 29, 2025
1 parent d7086e6 commit bea6e56
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 34 deletions.
2 changes: 1 addition & 1 deletion parrot/.changeset/v0.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ BenchmarkRegisterRoute-14 3647503 313.8 ns/op
BenchmarkRouteResponse-14 19143 62011 ns/op
BenchmarkSave-14 5244 218697 ns/op
BenchmarkLoad-14 1101 1049399 ns/op
```
```
6 changes: 3 additions & 3 deletions parrot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ A simple, high-performing mockserver that can dynamically build new routes with

## Features

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

## Use

Expand Down
4 changes: 1 addition & 3 deletions parrot/cage.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,7 @@ func (cl *cageLevel) route(routeSegment, routeMethod string) (route *Route, foun
if route, found = cl.routes[routeSegment][routeMethod]; found {
return route, true, nil
}
}
if _, ok := cl.wildCardRoutes[routeSegment]; ok {
if route, found = cl.wildCardRoutes[routeSegment][MethodAny]; found {
if route, found = cl.routes[routeSegment][MethodAny]; found { // Fallthrough to any method if it's designed
return route, true, nil
}
}
Expand Down
2 changes: 1 addition & 1 deletion parrot/cage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestCageNewRoutes(t *testing.T) {
func TestCage(t *testing.T) {
t.Parallel()

testCases := []struct {
Expand Down
101 changes: 101 additions & 0 deletions parrot/examples_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,107 @@ func ExampleServer_Register_internal() {
// 0
}

func ExampleServer_Register_wildcards() {
// 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))
if err != nil {
panic(err)
}
defer func() { // Cleanup the parrot instance
err = p.Shutdown(context.Background()) // Gracefully shutdown the parrot instance
if err != nil {
panic(err)
}
p.WaitShutdown() // Wait for the parrot instance to shutdown. Usually unnecessary, but we want to clean up the save file
os.Remove(saveFile) // Cleanup the save file for the example
}()

// You can use the MethodAny constant to match any HTTP method
anyMethodRoute := &parrot.Route{
Method: parrot.MethodAny,
Path: "/any-method",
RawResponseBody: "Any Method",
ResponseStatusCode: http.StatusOK,
}

err = p.Register(anyMethodRoute)
if err != nil {
panic(err)
}
resp, err := p.Call(http.MethodGet, "/any-method")
if err != nil {
panic(err)
}
fmt.Println(resp.Request.Method, string(resp.Body()))
resp, err = p.Call(http.MethodPost, "/any-method")
if err != nil {
panic(err)
}
fmt.Println(resp.Request.Method, string(resp.Body()))

// A * in the path will match any characters in the path
basicWildCard := &parrot.Route{
Method: parrot.MethodAny,
Path: "/wildcard/*",
RawResponseBody: "Basic Wildcard",
ResponseStatusCode: http.StatusOK,
}

err = p.Register(basicWildCard)
if err != nil {
panic(err)
}
resp, err = p.Call(http.MethodGet, "/wildcard/anything")
if err != nil {
panic(err)
}
fmt.Println(resp.Request.RawRequest.URL.Path, string(resp.Body()))

// Wild cards can be nested
nestedWildCardRoute := &parrot.Route{
Method: parrot.MethodAny,
Path: "/wildcard/*/nested/*",
RawResponseBody: "Nested Wildcard",
ResponseStatusCode: http.StatusOK,
}

err = p.Register(nestedWildCardRoute)
if err != nil {
panic(err)
}
resp, err = p.Call(http.MethodGet, "/wildcard/anything/nested/else")
if err != nil {
panic(err)
}
fmt.Println(resp.Request.RawRequest.URL.Path, string(resp.Body()))

// Wild cards can also be partials
partialWildCardRoute := &parrot.Route{
Method: parrot.MethodAny,
Path: "/partial*/wildcard",
RawResponseBody: "Partial Wildcard",
ResponseStatusCode: http.StatusOK,
}

err = p.Register(partialWildCardRoute)
if err != nil {
panic(err)
}
resp, err = p.Call(http.MethodGet, "/partial_anything/wildcard")
if err != nil {
panic(err)
}
fmt.Println(resp.Request.RawRequest.URL.Path, string(resp.Body()))

// Output:
// GET Any Method
// POST Any Method
// /wildcard/anything Basic Wildcard
// /wildcard/anything/nested/else Nested Wildcard
// /partial_anything/wildcard Partial Wildcard
}

func ExampleServer_Register_external() {
var (
saveFile = "route_example.json"
Expand Down
59 changes: 42 additions & 17 deletions parrot/parrot.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -322,8 +321,8 @@ func (p *Server) Register(route *Route) error {
if route == nil {
return ErrNilRoute
}
if !isValidPath(route.Path) {
return newDynamicError(ErrInvalidPath, fmt.Sprintf("'%s'", route.Path))
if err := checkPath(route.Path); err != nil {
return newDynamicError(ErrInvalidPath, err.Error())
}
if route.Method == "" {
return ErrNoMethod
Expand Down Expand Up @@ -400,6 +399,10 @@ func (p *Server) Delete(route *Route) error {
}

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

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

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

func isValidPath(path string) bool {
func checkPath(path string) error {
switch path {
case "", "/", "//", healthRoute, recordRoute, routesRoute, "/..":
return false
return fmt.Errorf("cannot match special paths: '%s'", path)
}
if strings.Contains(path, "/..") {
return false
return fmt.Errorf("cannot match parent directory traversal: '%s'", path)
}
if strings.Contains(path, "/.") {
return false
return fmt.Errorf("cannot match hidden files: '%s'", path)
}
if strings.Contains(path, "//") {
return false
return fmt.Errorf("cannot match double slashes: '%s'", path)
}
if !strings.HasPrefix(path, "/") {
return false
return fmt.Errorf("path must start with a forward slash: '%s'", path)
}
if strings.HasSuffix(path, "/") {
return false
return fmt.Errorf("path cannot end with a forward slash: '%s'", path)
}
if strings.HasPrefix(path, recordRoute) {
return false
return fmt.Errorf("cannot match record route: '%s'", path)
}
if strings.HasPrefix(path, healthRoute) {
return false
return fmt.Errorf("cannot match health route: '%s'", path)
}
if strings.HasPrefix(path, routesRoute) {
return false
return fmt.Errorf("cannot match routes route: '%s'", path)
}
match, err := filepath.Match(path, healthRoute)
if err != nil {
return fmt.Errorf("failed to match: '%s'", path)
}
if match {
return fmt.Errorf("cannot match health route: '%s'", path)
}

match, err = filepath.Match(path, recordRoute)
if err != nil {
return fmt.Errorf("failed to match: '%s'", path)
}
return validPathRegex.MatchString(path)
if match {
return fmt.Errorf("cannot match record route: '%s'", path)
}

match, err = filepath.Match(path, routesRoute)
if err != nil {
return fmt.Errorf("failed to match: '%s'", path)
}
if match {
return fmt.Errorf("cannot match routes route: '%s'", path)
}

return nil
}
8 changes: 8 additions & 0 deletions parrot/parrot_benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ func BenchmarkRegisterRoute(b *testing.B) {
b.StopTimer()
}

func BenchmarkRegisterWildCardRoute(b *testing.B) {
// TODO: Implement
}

func BenchmarkRouteResponse(b *testing.B) {
saveFile := b.Name() + ".json"
p, err := Wake(WithLogLevel(testLogLevel), WithSaveFile(saveFile))
Expand Down Expand Up @@ -72,6 +76,10 @@ func BenchmarkRouteResponse(b *testing.B) {
b.StopTimer()
}

func BenchmarkWildCardRouteResponse(b *testing.B) {

}

func BenchmarkSave(b *testing.B) {
var (
routes = []*Route{}
Expand Down
Loading

0 comments on commit bea6e56

Please sign in to comment.