parrot

package module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 27, 2025 License: MIT Imports: 20 Imported by: 4

README

Parrot Server

A simple, high-performing mockserver that can dynamically build new routes with customized responses, parroting back whatever you tell it to.

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

Use

See our runnable examples in examples_test.go to see how to use Parrot programmatically.

Run

go run ./cmd
go run ./cmd -h # See all config options 

Test

make test
make test PARROT_TEST_LOG_LEVEL=trace # Set log level for tests
make test_race # Test with -race flag enabled
make bench # Benchmark

Build

make goreleaser # Uses goreleaser to build binaries and docker containers

Documentation

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	ErrNilRoute        = errors.New("route is nil")
	ErrNoMethod        = errors.New("no method specified")
	ErrInvalidPath     = errors.New("invalid path")
	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")

	ErrNoRecorderURL      = errors.New("no recorder URL specified")
	ErrInvalidRecorderURL = errors.New("invalid recorder URL")
	ErrRecorderNotFound   = errors.New("recorder not found")

	ErrServerShutdown = errors.New("parrot is already asleep")
)

Functions

This section is empty.

Types

type Recorder

type Recorder struct {
	Host string `json:"host"`
	Port string `json:"port"`
	// contains filtered or unexported fields
}

Recorder records route calls

Example (External)

Example of how to use parrot recording when calling it from an external service

var (
	saveFile = "recorder_example.json"
	port     = 9091
)
defer os.Remove(saveFile) // Cleanup the save file for the example

go func() { // Run the parrot server as a separate instance, like in a Docker container
	_, err := parrot.Wake(parrot.WithPort(port), parrot.WithLogLevel(zerolog.NoLevel), parrot.WithSaveFile(saveFile))
	if err != nil {
		panic(err)
	}
}()

client := resty.New()
client.SetBaseURL(fmt.Sprintf("http://localhost:%d", port)) // The URL of the parrot server

waitForParrotServer(client, time.Second) // Wait for the parrot server to start

// Register a new route /test that will return a 200 status code with a text/plain response body of "Squawk"
route := &parrot.Route{
	Method:             http.MethodGet,
	Path:               "/test",
	RawResponseBody:    "Squawk",
	ResponseStatusCode: http.StatusOK,
}

// Register the route with the parrot instance
resp, err := client.R().SetBody(route).Post("/routes")
if err != nil {
	panic(err)
}

// Use the host of the machine your recorder is running on
// This should not be localhost if you are running the parrot server on a different machine
// It should be the public IP address of the machine running your code, so that the parrot can call back to it
host := "localhost"

// Create a new recorder with our host
recorder, err := parrot.NewRecorder(parrot.WithHost(host))
if err != nil {
	panic(err)
}

// Register the recorder with the parrot instance
resp, err = client.R().SetBody(recorder).Post("/record")
if err != nil {
	panic(err)
}
if resp.StatusCode() != http.StatusCreated {
	panic(fmt.Sprintf("failed to register recorder, got %d status code", resp.StatusCode()))
}

go func() { // Some other service calls the /test route
	_, err := client.R().Get("/test")
	if err != nil {
		panic(err)
	}
}()

// You can now listen to the recorder for all route calls
for {
	select {
	case recordedRouteCall := <-recorder.Record():
		if recordedRouteCall.RouteID == route.ID() {
			fmt.Println(recordedRouteCall.RouteID)
			fmt.Println(recordedRouteCall.Request.Method)
			fmt.Println(recordedRouteCall.Response.StatusCode)
			fmt.Println(string(recordedRouteCall.Response.Body))
			return
		}
	case err := <-recorder.Err():
		panic(err)
	}
}
Output:

GET:/test
GET
200
Squawk
Example (Internal)
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"

	"github.com/rs/zerolog"
	"github.com/smartcontractkit/chainlink-testing-framework/parrot"
)

func main() {
	saveFile := "recorder_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
	}()

	// Create a new recorder
	recorder, err := parrot.NewRecorder()
	if err != nil {
		panic(err)
	}

	// Register the recorder with the parrot instance
	err = p.Record(recorder.URL())
	if err != nil {
		panic(err)
	}
	defer recorder.Close()

	// Register a new route /test that will return a 200 status code with a text/plain response body of "Squawk"
	route := &parrot.Route{
		Method:             http.MethodGet,
		Path:               "/test",
		RawResponseBody:    "Squawk",
		ResponseStatusCode: http.StatusOK,
	}
	err = p.Register(route)
	if err != nil {
		panic(err)
	}

	// Call the route
	go func() {
		_, err := p.Call(http.MethodGet, "/test")
		if err != nil {
			panic(err)
		}
	}()

	// Record the route call
	for {
		select {
		case recordedRouteCall := <-recorder.Record():
			if recordedRouteCall.RouteID == route.ID() {
				fmt.Println(recordedRouteCall.RouteID)
				fmt.Println(recordedRouteCall.Request.Method)
				fmt.Println(recordedRouteCall.Response.StatusCode)
				fmt.Println(string(recordedRouteCall.Response.Body))
				return
			}
		case err := <-recorder.Err():
			panic(err)
		}
	}
}
Output:

GET:/test
GET
200
Squawk

func NewRecorder

func NewRecorder(opts ...RecorderOption) (*Recorder, error)

NewRecorder creates a new recorder that listens for incoming requests to the parrot server

func (*Recorder) Close

func (r *Recorder) Close() error

Close shuts down the recorder

func (*Recorder) Err

func (r *Recorder) Err() chan error

Err receives errors from the recorder

func (*Recorder) Record

func (r *Recorder) Record() chan *RouteCall

Record receives recorded calls

func (*Recorder) URL

func (r *Recorder) URL() string

URL returns the URL of the recorder to send requests to WARNING: This URL automatically binds to the first available port on the host machine and the host will be 0.0.0.0 or localhost. If you're calling this from a different machine you will need to replace the host with the IP address of the machine running the recorder.

type RecorderOption

type RecorderOption func(*Recorder)

RecorderOption is a function that modifies a recorder

func WithHost

func WithHost(host string) RecorderOption

WithHost sets the host of the recorder

type Route

type Route struct {
	// Method is the HTTP method to match
	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
	ResponseBody any `json:"response_body"`
	// ResponseStatusCode is the HTTP status code to return when called
	ResponseStatusCode int `json:"response_status_code"`
}

Route holds information about the mock route configuration

func (*Route) ID

func (r *Route) ID() string

ID returns the unique identifier for the route

type RouteCall

type RouteCall struct {
	// RouteCallID is a unique identifier for the route call for help with debugging
	RouteCallID string `json:"route_call_id"`
	// RouteID is the identifier of the route that was called
	RouteID string `json:"route_id"`
	// Request is the request made to the route
	Request *RouteCallRequest `json:"request"`
	// Response is the response from the route
	Response *RouteCallResponse `json:"response"`
}

RouteCall records when a route is called, the request and response

type RouteCallRequest

type RouteCallRequest struct {
	Method     string      `json:"method"`
	URL        *url.URL    `json:"url"`
	RemoteAddr string      `json:"caller"`
	Header     http.Header `json:"header"`
	Body       []byte      `json:"body"`
}

RouteCallRequest records the request made to a route

type RouteCallResponse

type RouteCallResponse struct {
	StatusCode int         `json:"status_code"`
	Header     http.Header `json:"header"`
	Body       []byte      `json:"body"`
}

RouteCallResponse records the response from a route

type SaveFile

type SaveFile struct {
	Routes    []*Route `json:"routes"`
	Recorders []string `json:"recorders"`
}

SaveFile is the structure of the file to save and load parrot data from

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server is a mock HTTP server that can register and respond to dynamic routes

func Wake

func Wake(options ...ServerOption) (*Server, error)

Wake creates a new Parrot server with dynamic route handling

func (*Server) Address

func (p *Server) Address() string

Address returns the address the parrot is running on

func (*Server) Call

func (p *Server) Call(method, path string) (*resty.Response, error)

Call makes a request to the parrot server

func (*Server) Delete

func (p *Server) Delete(routeID string) error

Delete removes a route from the parrot

func (*Server) Record

func (p *Server) Record(recorderURL string) error

Record registers a new recorder with the parrot. All incoming requests to the parrot will be sent to the recorder.

func (*Server) Recorders

func (p *Server) Recorders() []string

Recorders returns the URLs of all registered recorders

func (*Server) Register

func (p *Server) Register(route *Route) error

Register adds a new route to the parrot

Example (External)
var (
	saveFile = "route_example.json"
	port     = 9090
)
defer os.Remove(saveFile) // Cleanup the save file for the example

go func() { // Run the parrot server as a separate instance, like in a Docker container
	_, err := parrot.Wake(parrot.WithPort(port), parrot.WithLogLevel(zerolog.NoLevel), parrot.WithSaveFile(saveFile))
	if err != nil {
		panic(err)
	}
}()

// Code that calls the parrot server from another service
// Use resty to make HTTP calls to the parrot server
client := resty.New()
client.SetBaseURL(fmt.Sprintf("http://localhost:%d", port)) // The URL of the parrot server

waitForParrotServer(client, time.Second) // Wait for the parrot server to start

// Register a new route /test that will return a 200 status code with a text/plain response body of "Squawk"
route := &parrot.Route{
	Method:             http.MethodGet,
	Path:               "/test",
	RawResponseBody:    "Squawk",
	ResponseStatusCode: http.StatusOK,
}
resp, err := client.R().SetBody(route).Post("/routes")
if err != nil {
	panic(err)
}
defer resp.RawResponse.Body.Close()
fmt.Println(resp.StatusCode())

// Get all routes from the parrot server
routes := make([]*parrot.Route, 0)
resp, err = client.R().SetResult(&routes).Get("/routes")
if err != nil {
	panic(err)
}
defer resp.RawResponse.Body.Close()
fmt.Println(resp.StatusCode())
fmt.Println(len(routes))

// Delete the route
resp, err = client.R().SetBody(route).Delete("/routes")
if err != nil {
	panic(err)
}
defer resp.RawResponse.Body.Close()
fmt.Println(resp.StatusCode())

// Get all routes from the parrot server
routes = make([]*parrot.Route, 0)
resp, err = client.R().SetResult(&routes).Get("/routes")
if err != nil {
	panic(err)
}
defer resp.RawResponse.Body.Close()
fmt.Println(len(routes))
Output:

201
200
1
204
0
Example (Internal)
package main

import (
	"context"
	"fmt"
	"net/http"
	"os"

	"github.com/rs/zerolog"
	"github.com/smartcontractkit/chainlink-testing-framework/parrot"
)

func main() {
	// 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
	}()

	// Create a new route /test that will return a 200 status code with a text/plain response body of "Squawk"
	route := &parrot.Route{
		Method:             http.MethodGet,
		Path:               "/test",
		RawResponseBody:    "Squawk",
		ResponseStatusCode: http.StatusOK,
	}

	// Register the route with the parrot instance
	err = p.Register(route)
	if err != nil {
		panic(err)
	}

	// Call the route
	resp, err := p.Call(http.MethodGet, "/test")
	if err != nil {
		panic(err)
	}
	fmt.Println(resp.StatusCode())
	fmt.Println(string(resp.Body()))

	// Get all routes from the parrot instance
	routes := p.Routes()
	fmt.Println(len(routes))

	// Delete the route
	err = p.Delete(route.ID())
	if err != nil {
		panic(err)
	}

	// Get all routes from the parrot instance
	routes = p.Routes()
	fmt.Println(len(routes))
}
Output:

200
Squawk
1
0

func (*Server) Routes

func (p *Server) Routes() []*Route

func (*Server) Shutdown

func (p *Server) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the parrot server

func (*Server) WaitShutdown

func (p *Server) WaitShutdown()

WaitShutdown blocks until the parrot server has shut down

type ServerOption

type ServerOption func(*Server) error

ServerOption defines functional options for configuring the ParrotServer

func DisableConsoleLogs added in v0.1.7

func DisableConsoleLogs() ServerOption

DisableConsoleLogs disables logging to the console

func WithJSONLogs

func WithJSONLogs() ServerOption

WithJSONLogs sets the logger to output JSON logs

func WithLogFile

func WithLogFile(logFile string) ServerOption

WithLogFile sets the file to save the logs to

func WithLogLevel

func WithLogLevel(level zerolog.Level) ServerOption

WithLogLevel sets the visible log level of the default logger

func WithLogger

func WithLogger(l zerolog.Logger) ServerOption

WithLogger sets the logger for the ParrotServer

func WithPort

func WithPort(port int) ServerOption

WithPort sets the port for the ParrotServer to run on

func WithRoutes

func WithRoutes(routes []*Route) ServerOption

WithRoutes sets the initial routes for the Parrot

func WithSaveFile

func WithSaveFile(saveFile string) ServerOption

WithSaveFile sets the file to save the routes to

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL