Skip to content

Commit b523706

Browse files
committed
Adds recorder
1 parent 79453fa commit b523706

File tree

7 files changed

+263
-121
lines changed

7 files changed

+263
-121
lines changed

parrot/examples_test.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ func ExampleServer() {
1818

1919
// Create a new route /test that will return a 200 status code with a text/plain response body of "Squawk"
2020
route := &parrot.Route{
21-
Method: http.MethodGet,
22-
Path: "/test",
23-
RawResponseBody: "Squawk",
24-
ResponseStatusCode: 200,
25-
ResponseContentType: "text/plain",
21+
Method: http.MethodGet,
22+
Path: "/test",
23+
RawResponseBody: "Squawk",
24+
ResponseStatusCode: 200,
2625
}
2726

2827
// Register the route with the parrot instance
@@ -39,11 +38,9 @@ func ExampleServer() {
3938
defer resp.Body.Close()
4039

4140
fmt.Println(resp.StatusCode)
42-
fmt.Println(resp.Header.Get("Content-Type"))
4341
body, _ := io.ReadAll(resp.Body)
4442
fmt.Println(string(body))
4543
// Output:
4644
// 200
47-
// text/plain
4845
// Squawk
4946
}

parrot/go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/smartcontractkit/chainlink-testing-framework/parrot
33
go 1.23.4
44

55
require (
6+
github.com/go-resty/resty/v2 v2.16.3
67
github.com/rs/zerolog v1.33.0
78
github.com/spf13/cobra v1.8.1
89
github.com/stretchr/testify v1.9.0
@@ -15,6 +16,7 @@ require (
1516
github.com/mattn/go-isatty v0.0.19 // indirect
1617
github.com/pmezard/go-difflib v1.0.0 // indirect
1718
github.com/spf13/pflag v1.0.5 // indirect
18-
golang.org/x/sys v0.12.0 // indirect
19+
golang.org/x/net v0.33.0 // indirect
20+
golang.org/x/sys v0.28.0 // indirect
1921
gopkg.in/yaml.v3 v3.0.1 // indirect
2022
)

parrot/go.sum

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
22
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
33
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
44
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
6+
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
57
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
68
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
79
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
@@ -23,10 +25,15 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
2325
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
2426
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2527
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
28+
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
29+
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
2630
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
2731
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
28-
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
2932
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
33+
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
34+
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
35+
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
36+
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
3037
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
3138
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3239
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

parrot/parrot.go

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"sync"
1818
"time"
1919

20+
"github.com/go-resty/resty/v2"
2021
"github.com/rs/zerolog"
2122
)
2223

@@ -35,14 +36,16 @@ type Route struct {
3536
ResponseBody any `json:"response_body"`
3637
// ResponseStatusCode is the HTTP status code to return when called
3738
ResponseStatusCode int `json:"response_status_code"`
38-
// ResponseContentType is the Content-Type header to return the response with
39-
ResponseContentType string `json:"response_content_type"`
4039
}
4140

42-
// RouteRequestBody is the request body for querying the server on a specific route
43-
type RouteRequestBody struct {
44-
Method string `json:"method"`
45-
Path string `json:"path"`
41+
// ID returns the unique identifier for the route
42+
func (r *Route) ID() string {
43+
return r.Method + ":" + r.Path
44+
}
45+
46+
// RouteRequest is the request body for querying the server on a specific route
47+
type RouteRequest struct {
48+
ID string `json:"id"`
4649
}
4750

4851
// Server is a mock HTTP server that can register and respond to dynamic routes
@@ -286,7 +289,7 @@ func (p *Server) Register(route *Route) error {
286289

287290
p.routesMu.Lock()
288291
defer p.routesMu.Unlock()
289-
p.routes[route.Method+":"+route.Path] = route
292+
p.routes[route.ID()] = route
290293

291294
return nil
292295
}
@@ -295,20 +298,20 @@ func (p *Server) Register(route *Route) error {
295298
func (p *Server) registerRouteHandler(w http.ResponseWriter, r *http.Request) {
296299
const parrotPath = "/register"
297300
if r.Method == http.MethodDelete {
298-
var routeRequestBody *RouteRequestBody
299-
if err := json.NewDecoder(r.Body).Decode(&routeRequestBody); err != nil {
301+
var routeRequest *RouteRequest
302+
if err := json.NewDecoder(r.Body).Decode(&routeRequest); err != nil {
300303
http.Error(w, "Invalid request body", http.StatusBadRequest)
301304
return
302305
}
303306
defer r.Body.Close()
304307

305-
if routeRequestBody.Method == "" || routeRequestBody.Path == "" {
306-
err := errors.New("Method and path are required")
308+
if routeRequest.ID == "" {
309+
err := errors.New("ID required")
307310
http.Error(w, err.Error(), http.StatusBadRequest)
308311
return
309312
}
310313

311-
err := p.Unregister(routeRequestBody.Method, routeRequestBody.Path)
314+
err := p.Unregister(routeRequest.ID)
312315
if err != nil {
313316
http.Error(w, err.Error(), http.StatusBadRequest)
314317
p.log.Trace().Err(err).Str("Path", parrotPath).Msg("Failed to unregister route")
@@ -317,9 +320,8 @@ func (p *Server) registerRouteHandler(w http.ResponseWriter, r *http.Request) {
317320

318321
w.WriteHeader(http.StatusNoContent)
319322
p.log.Info().
320-
Str("Route Path", routeRequestBody.Path).
323+
Str("Route ID", routeRequest.ID).
321324
Str("Parrot Path", parrotPath).
322-
Str("Method", routeRequestBody.Method).
323325
Msg("Route unregistered")
324326
} else if r.Method == http.MethodPost {
325327
var route *Route
@@ -389,25 +391,18 @@ func (p *Server) recordHandler(w http.ResponseWriter, r *http.Request) {
389391
p.log.Info().Str("Recorder URL", recorder.URL).Str("Parrot Path", parrotPath).Msg("Recorder added")
390392
}
391393

392-
// Routes returns all registered routes
393-
func (p *Server) Routes() map[string]*Route {
394-
p.routesMu.RLock()
395-
defer p.routesMu.RUnlock()
396-
return p.routes
397-
}
398-
399394
// Unregister removes a route from the parrot
400-
func (p *Server) Unregister(method, path string) error {
395+
func (p *Server) Unregister(routeID string) error {
401396
p.routesMu.RLock()
402-
_, exists := p.routes[method+":"+path]
397+
_, exists := p.routes[routeID]
403398
p.routesMu.RUnlock()
404399

405400
if !exists {
406-
return newDynamicError(ErrRouteNotFound, fmt.Sprintf("%s %s", method, path))
401+
return newDynamicError(ErrRouteNotFound, routeID)
407402
}
408403
p.routesMu.Lock()
409404
defer p.routesMu.Unlock()
410-
delete(p.routes, method+":"+path)
405+
delete(p.routes, routeID)
411406
return nil
412407
}
413408

@@ -428,30 +423,36 @@ func (p *Server) dynamicHandler(w http.ResponseWriter, r *http.Request) {
428423
route, exists := p.routes[r.Method+":"+r.URL.Path]
429424
p.routesMu.RUnlock()
430425

431-
if !exists {
432-
http.NotFound(w, r)
433-
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Route not found")
434-
return
426+
routeCall := &RouteCall{
427+
RouteID: r.Method + ":" + r.URL.Path,
428+
Request: r,
435429
}
430+
recordingWriter := newResponseWriterRecorder(w)
431+
432+
defer func() {
433+
routeCall.Response = recordingWriter.Result()
434+
p.sendToRecorders(routeCall)
435+
}()
436436

437-
if route.ResponseContentType != "" {
438-
w.Header().Set("Content-Type", route.ResponseContentType)
437+
if !exists { // Route not found
438+
http.NotFound(recordingWriter, r)
439+
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Route not found")
440+
return
439441
}
440-
w.WriteHeader(route.ResponseStatusCode)
441442

443+
// Let the custom handler take over if it exists
442444
if route.Handler != nil {
443445
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Calling route handler")
444-
route.Handler(w, r)
446+
route.Handler(recordingWriter, r)
445447
return
446448
}
447449

450+
recordingWriter.WriteHeader(route.ResponseStatusCode)
451+
448452
if route.RawResponseBody != "" {
449-
if route.ResponseContentType == "" {
450-
w.Header().Set("Content-Type", "text/plain")
451-
}
452453
if _, err := w.Write([]byte(route.RawResponseBody)); err != nil {
453454
p.log.Trace().Err(err).Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Failed to write response")
454-
http.Error(w, "Failed to write response", http.StatusInternalServerError)
455+
http.Error(recordingWriter, "Failed to write response", http.StatusInternalServerError)
455456
return
456457
}
457458
p.log.Trace().
@@ -464,17 +465,14 @@ func (p *Server) dynamicHandler(w http.ResponseWriter, r *http.Request) {
464465
}
465466

466467
if route.ResponseBody != nil {
467-
if route.ResponseContentType == "" {
468-
w.Header().Set("Content-Type", "application/json")
469-
}
470468
rawJSON, err := json.Marshal(route.ResponseBody)
471469
if err != nil {
472470
p.log.Trace().Err(err).
473471
Str("Remote Addr", r.RemoteAddr).
474472
Str("Path", r.URL.Path).
475473
Str("Method", r.Method).
476474
Msg("Failed to marshal JSON response")
477-
http.Error(w, "Failed to marshal response into json", http.StatusInternalServerError)
475+
http.Error(recordingWriter, "Failed to marshal response into json", http.StatusInternalServerError)
478476
return
479477
}
480478
if _, err = w.Write(rawJSON); err != nil {
@@ -484,7 +482,7 @@ func (p *Server) dynamicHandler(w http.ResponseWriter, r *http.Request) {
484482
Str("Path", r.URL.Path).
485483
Str("Method", r.Method).
486484
Msg("Failed to write response")
487-
http.Error(w, "Failed to write JSON response", http.StatusInternalServerError)
485+
http.Error(recordingWriter, "Failed to write JSON response", http.StatusInternalServerError)
488486
return
489487
}
490488
p.log.Trace().
@@ -552,6 +550,34 @@ func (p *Server) save() error {
552550
return nil
553551
}
554552

553+
// sendToRecorders sends the route call to all registered recorders
554+
func (p *Server) sendToRecorders(routeCall *RouteCall) {
555+
p.recordersMu.RLock()
556+
defer p.recordersMu.RUnlock()
557+
558+
client := resty.New()
559+
560+
for _, hook := range p.recorderHooks {
561+
go func(hook string) {
562+
p.log.Trace().Str("Recorder Hook", hook).Msg("Sending route call to recorder")
563+
resp, err := client.R().SetBody(routeCall).Post(hook)
564+
if err != nil {
565+
p.log.Error().Err(err).Str("Recorder Hook", hook).Msg("Failed to send route call to recorder")
566+
return
567+
}
568+
if resp.IsError() {
569+
p.log.Error().
570+
Str("Recorder Hook", hook).
571+
Int("Code", resp.StatusCode()).
572+
Str("Response", resp.String()).
573+
Msg("Failed to send route call to recorder")
574+
return
575+
}
576+
p.log.Debug().Str("Recorder Hook", hook).Msg("Route call sent to recorder")
577+
}(hook)
578+
}
579+
}
580+
555581
var pathRegex = regexp.MustCompile(`^\/[a-zA-Z0-9\-._~%!$&'()*+,;=:@\/]*$`)
556582

557583
func isValidPath(path string) bool {

0 commit comments

Comments
 (0)