Documentation
¶
Index ¶
- Constants
- Variables
- type Client
- func (c *Client) CallRoute(method, path string) (*resty.Response, error)
- func (c *Client) DeleteRoute(route *Route) error
- func (c *Client) Healthy() (bool, error)
- func (c *Client) Recorders() ([]string, error)
- func (c *Client) RegisterRecorder(recorder *Recorder) error
- func (c *Client) RegisterRoute(route *Route) error
- func (c *Client) Routes() ([]*Route, error)
- type Recorder
- type RecorderOption
- type Route
- type RouteCall
- type RouteCallRequest
- type RouteCallResponse
- type SaveFile
- type Server
- func (p *Server) Address() string
- func (p *Server) Call(method, path string) (*resty.Response, error)
- func (p *Server) Delete(route *Route)
- func (p *Server) Healthy() error
- func (p *Server) Host() string
- func (p *Server) Port() int
- func (p *Server) Record(recorderURL string) error
- func (p *Server) Recorders() []string
- func (p *Server) Register(route *Route) error
- func (p *Server) Routes() []*Route
- func (p *Server) Shutdown(ctx context.Context) error
- func (p *Server) WaitShutdown()
- type ServerOption
- func DisableConsoleLogs() ServerOption
- func WithHost(host string) ServerOption
- func WithJSONLogs() ServerOption
- func WithLogFile(logFile string) ServerOption
- func WithLogLevel(level zerolog.Level) ServerOption
- func WithPort(port int) ServerOption
- func WithRecorders(recorders ...string) ServerOption
- func WithRoutes(routes []*Route) ServerOption
- func WithSaveFile(saveFile string) ServerOption
Examples ¶
Constants ¶
const ( HealthRoute = "/health" RoutesRoute = "/routes" RecorderRoute = "/recorder" // MethodAny is a wildcard for any HTTP method MethodAny = "ANY" )
Variables ¶
var ( ErrNilRoute = errors.New("route is nil") 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") ErrRecorderNotFound = errors.New("recorder not found") ErrServerShutdown = errors.New("parrot is already asleep") ErrServerUnhealthy = errors.New("parrot is unhealthy") )
Functions ¶
This section is empty.
Types ¶
type Client ¶ added in v0.4.0
type Client struct {
// contains filtered or unexported fields
}
Client interacts with a parrot server
func NewClient ¶ added in v0.4.0
NewClient creates a new client for a parrot server running at the given url.
func (*Client) DeleteRoute ¶ added in v0.4.0
DeleteRoute deletes a route on the server
func (*Client) Recorders ¶ added in v0.4.0
Recorders returns all the recorders registered on the server
func (*Client) RegisterRecorder ¶ added in v0.4.0
RegisterRecorder registers a recorder on the server
func (*Client) RegisterRoute ¶ added in v0.4.0
RegisterRoute registers a route on the server
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
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/rs/zerolog"
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
)
func main() {
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.NewServer(parrot.WithPort(port), parrot.WithLogLevel(zerolog.NoLevel), parrot.WithSaveFile(saveFile))
if err != nil {
panic(err)
}
}()
client := parrot.NewClient(fmt.Sprintf("http://localhost:%d", port))
waitForParrotServerExternal(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
err := client.RegisterRoute(route)
if err != nil {
panic(err)
}
// Use the recorderHost 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
recorderHost := "localhost"
// Create a new recorder with our host
recorder, err := parrot.NewRecorder(parrot.WithRecorderHost(recorderHost))
if err != nil {
panic(err)
}
// Register the recorder with the parrot instance
err = client.RegisterRecorder(recorder)
if err != nil {
panic(err)
}
recorders, err := client.Recorders()
if err != nil {
panic(err)
}
fmt.Printf("Found %d recorders\n", len(recorders))
go func() { // Some other service calls the /test route
_, err := client.CallRoute(http.MethodGet, "/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)
}
}
}
// waitForParrotServerExternal checks the parrot server health endpoint until it returns a 200 status code or the timeout is reached
func waitForParrotServerExternal(client *parrot.Client, timeoutDur time.Duration) {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
timeout := time.NewTimer(timeoutDur)
for {
select {
case <-ticker.C:
healthy, err := client.Healthy()
if err != nil {
continue
}
if healthy {
return
}
case <-timeout.C:
panic("timeout waiting for parrot server to start")
}
}
}
Output: Found 1 recorders GET:/test GET 200 Squawk
Example (Internal) ¶
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
"github.com/rs/zerolog"
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
)
func main() {
saveFile := "recorder_example.json"
p, err := parrot.NewServer(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)
}
waitForParrotServerInternal(p, time.Second) // Wait for the parrot server to start
// 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)
}
}
}
func waitForParrotServerInternal(p *parrot.Server, timeoutDur time.Duration) {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
timeout := time.NewTimer(timeoutDur)
for {
select {
case <-ticker.C:
if err := p.Healthy(); err == nil {
return
}
case <-timeout.C:
panic("timeout waiting for parrot server to start")
}
}
}
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) URL ¶
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 WithRecorderHost ¶ added in v0.6.1
func WithRecorderHost(host string) RecorderOption
WithRecorderHost 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"`
// 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
type RouteCall ¶
type RouteCall struct {
// ID is a unique identifier for the route call for help with debugging
ID string `json:"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 Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server is a mock HTTP server that can register and respond to dynamic routes
Example (External) ¶
package main
import (
"fmt"
"net/http"
"os"
"time"
"github.com/rs/zerolog"
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
)
func main() {
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.NewServer(parrot.WithPort(port), parrot.WithLogLevel(zerolog.NoLevel), parrot.WithSaveFile(saveFile))
if err != nil {
panic(err)
}
}()
// Get a client to interact with the parrot server
client := parrot.NewClient(fmt.Sprintf("http://localhost:%d", port))
waitForParrotServerExternal(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,
}
err := client.RegisterRoute(route)
if err != nil {
panic(err)
}
fmt.Println("Registered route")
// Get all routes from the parrot server
routes, err := client.Routes()
if err != nil {
panic(err)
}
fmt.Printf("Found %d routes\n", len(routes))
// Delete the route
err = client.DeleteRoute(route)
if err != nil {
panic(err)
}
fmt.Println("Deleted route")
// Get all routes from the parrot server
routes, err = client.Routes()
if err != nil {
panic(err)
}
fmt.Printf("Found %d routes\n", len(routes))
}
// waitForParrotServerExternal checks the parrot server health endpoint until it returns a 200 status code or the timeout is reached
func waitForParrotServerExternal(client *parrot.Client, timeoutDur time.Duration) {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
timeout := time.NewTimer(timeoutDur)
for {
select {
case <-ticker.C:
healthy, err := client.Healthy()
if err != nil {
continue
}
if healthy {
return
}
case <-timeout.C:
panic("timeout waiting for parrot server to start")
}
}
}
Output: Registered route Found 1 routes Deleted route Found 0 routes
Example (Internal) ¶
package main
import (
"context"
"fmt"
"net/http"
"os"
"time"
"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.NewServer(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,
}
waitForParrotServerInternal(p, time.Second) // Wait for the parrot server to start
// 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
p.Delete(route)
// Get all routes from the parrot instance
routes = p.Routes()
fmt.Println(len(routes))
}
func waitForParrotServerInternal(p *parrot.Server, timeoutDur time.Duration) {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
timeout := time.NewTimer(timeoutDur)
for {
select {
case <-ticker.C:
if err := p.Healthy(); err == nil {
return
}
case <-timeout.C:
panic("timeout waiting for parrot server to start")
}
}
}
Output: 200 Squawk 1 0
func NewServer ¶ added in v0.4.0
func NewServer(options ...ServerOption) (*Server, error)
NewServer creates a new Parrot server with dynamic route handling
func (*Server) Record ¶
Record registers a new recorder with the parrot. All incoming requests to the parrot will be sent to the recorder.
func (*Server) WaitShutdown ¶
func (p *Server) WaitShutdown()
WaitShutdown blocks until the parrot server has shut down
type ServerOption ¶
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 WithHost ¶
func WithHost(host string) ServerOption
WithHost sets the address for the ParrotServer to run on
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 WithPort ¶
func WithPort(port int) ServerOption
WithPort sets the port for the ParrotServer to run on
func WithRecorders ¶ added in v0.2.0
func WithRecorders(recorders ...string) ServerOption
WithRecorders sets the initial recorders for the Parrot
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