@@ -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 {
295298func (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+
555581var pathRegex = regexp .MustCompile (`^\/[a-zA-Z0-9\-._~%!$&'()*+,;=:@\/]*$` )
556582
557583func isValidPath (path string ) bool {
0 commit comments