Skip to content

Commit c6218da

Browse files
committed
Fixes tests adds examples
1 parent da8385e commit c6218da

File tree

5 files changed

+210
-79
lines changed

5 files changed

+210
-79
lines changed

parrot/Makefile

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# Default test log level (can be overridden)
2+
TEST_LOG_LEVEL ?= ""
3+
4+
# Pass TEST_LOG_LEVEL as a flag to go test
5+
TEST_ARGS ?= -testLogLevel=$(TEST_LOG_LEVEL)
6+
17
.PHONY: lint
28
lint:
39
golangci-lint --color=always run ./... --fix -v
@@ -6,18 +12,18 @@ lint:
612
test:
713
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
814
set -euo pipefail
9-
go test -json -cover -coverprofile cover.out -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
15+
go test $(TEST_ARGS) -json -cover -coverprofile cover.out -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
1016

1117
.PHONY: test_race
1218
test_race:
1319
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
1420
set -euo pipefail
15-
go test -json -cover -count=1 -race -coverprofile cover.out -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
21+
go test $(TEST_ARGS) -json -cover -count=1 -race -coverprofile cover.out -v ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
1622

1723
.PHONY: test_unit
1824
test_unit:
19-
go test -coverprofile cover.out ./...
25+
go test $(TEST_ARGS) -coverprofile cover.out ./...
2026

2127
.PHONY: bench
2228
bench:
23-
go test -bench=. -run=^$$ ./...
29+
go test $(TEST_ARGS) -bench=. -run=^$$ ./...

parrot/cmd/main.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os/signal"
77
"syscall"
88

9+
"github.com/rs/zerolog"
910
"github.com/rs/zerolog/log"
1011
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
1112
"github.com/spf13/cobra"
@@ -25,15 +26,17 @@ func main() {
2526
Short: "a server that can set and parrrot back dynamic requests",
2627
RunE: func(cmd *cobra.Command, args []string) error {
2728
options := []parrot.ServerOption{parrot.WithPort(port)}
29+
logLevel := zerolog.InfoLevel
2830
if debug {
29-
options = append(options, parrot.WithDebug())
31+
logLevel = zerolog.DebugLevel
3032
}
3133
if trace {
32-
options = append(options, parrot.WithTrace())
34+
logLevel = zerolog.TraceLevel
3335
}
3436
if silent {
35-
options = append(options, parrot.Silent())
37+
logLevel = zerolog.Disabled
3638
}
39+
options = append(options, parrot.WithLogLevel(logLevel))
3740
if json {
3841
options = append(options, parrot.WithJSONLogs())
3942
}

parrot/parrot.go

Lines changed: 99 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@ import (
1919
// Route holds information about the mock route configuration
2020
type Route struct {
2121
// Method is the HTTP method to match
22-
Method string `json:"method"`
22+
Method string `json:"Method"`
2323
// Path is the URL path to match
24-
Path string `json:"path"`
24+
Path string `json:"Path"`
2525
// Handler is the dynamic handler function to use when called
26+
// Can only be set upon creation of the server
2627
Handler http.HandlerFunc `json:"-"`
27-
// ResponseBody is the static JSON response to return when called
28+
// RawResponseBody is the static, raw string response to return when called
29+
RawResponseBody string `json:"raw_response_body"`
30+
// ResponseBody will be marshalled to JSON and returned when called
2831
ResponseBody any `json:"response_body"`
2932
// ResponseStatusCode is the HTTP status code to return when called
3033
ResponseStatusCode int `json:"response_status_code"`
@@ -52,30 +55,20 @@ type ServerOption func(*Server) error
5255
func WithPort(port int) ServerOption {
5356
return func(s *Server) error {
5457
if port == 0 {
55-
s.log.Debug().Msg("No port specified, using random port")
58+
s.log.Debug().Msg("Configuring Parrot: No port specified, using random port")
5659
} else if port < 0 || port > 65535 {
5760
return fmt.Errorf("invalid port: %d", port)
5861
}
5962
s.port = port
60-
s.log.Debug().Int("port", port).Msg("Setting port")
63+
s.log.Debug().Int("port", port).Msg("Configuring Parrot: Setting port")
6164
return nil
6265
}
6366
}
6467

65-
// WithDebug sets the log level to debug
66-
func WithDebug() ServerOption {
68+
func WithLogLevel(level zerolog.Level) ServerOption {
6769
return func(s *Server) error {
68-
s.log = s.log.Level(zerolog.DebugLevel)
69-
s.log.Debug().Msg("Setting log level to debug")
70-
return nil
71-
}
72-
}
73-
74-
// WithTrace sets the log level to trace
75-
func WithTrace() ServerOption {
76-
return func(s *Server) error {
77-
s.log = s.log.Level(zerolog.TraceLevel)
78-
s.log.Debug().Msg("Setting log level to trace")
70+
s.log = s.log.Level(level)
71+
s.log.Debug().Str("log level", level.String()).Msg("Configuring Parrot: Setting log level")
7972
return nil
8073
}
8174
}
@@ -84,15 +77,7 @@ func WithTrace() ServerOption {
8477
func WithLogger(l zerolog.Logger) ServerOption {
8578
return func(s *Server) error {
8679
s.log = l
87-
s.log.Debug().Msg("Setting custom logger")
88-
return nil
89-
}
90-
}
91-
92-
// Silent sets the logger to a no-op logger
93-
func Silent() ServerOption {
94-
return func(s *Server) error {
95-
s.log = zerolog.Nop()
80+
s.log.Debug().Msg("Configuring Parrot: Setting custom logger")
9681
return nil
9782
}
9883
}
@@ -101,7 +86,7 @@ func Silent() ServerOption {
10186
func WithJSONLogs() ServerOption {
10287
return func(s *Server) error {
10388
s.log = s.log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339Nano})
104-
s.log.Debug().Msg("Setting log output to JSON")
89+
s.log.Debug().Msg("Configuring Parrot: Setting log output to JSON")
10590
return nil
10691
}
10792
}
@@ -113,7 +98,7 @@ func WithSaveFile(saveFile string) ServerOption {
11398
return fmt.Errorf("invalid save file name: %s", saveFile)
11499
}
115100
s.saveFile = saveFile
116-
s.log.Debug().Str("file", saveFile).Msg("Setting save file")
101+
s.log.Debug().Str("file", saveFile).Msg("Configuring Parrot: Setting save file")
117102
return nil
118103
}
119104
}
@@ -125,7 +110,7 @@ func WithRoutes(routes []*Route) ServerOption {
125110
if err := s.Register(route); err != nil {
126111
return fmt.Errorf("failed to register route: %w", err)
127112
}
128-
s.log.Debug().Str("path", route.Path).Str("method", route.Method).Msg("Pre-registered route")
113+
s.log.Debug().Str("Path", route.Path).Str("Method", route.Method).Msg("Configuring Parrot: Pre-registered route")
129114
}
130115
return nil
131116
}
@@ -223,21 +208,35 @@ func (p *Server) Register(route *Route) error {
223208
if route.Method == "" {
224209
return fmt.Errorf("invalid route method: %s", route.Method)
225210
}
226-
if route.Handler == nil && route.ResponseBody == nil {
211+
if route.Handler == nil && route.ResponseBody == nil && route.RawResponseBody == "" {
227212
return fmt.Errorf("route must have a handler or response body")
228213
}
214+
if route.Handler != nil && (route.ResponseBody != nil || route.RawResponseBody != "") {
215+
return fmt.Errorf("route cannot have both a handler and response body")
216+
}
217+
if route.ResponseBody != nil && route.RawResponseBody != "" {
218+
return fmt.Errorf("route cannot have both a response body and raw response body")
219+
}
229220
if route.ResponseBody != nil {
230221
if _, err := json.Marshal(route.ResponseBody); err != nil {
231-
return fmt.Errorf("failed to marshal response body: %w", err)
222+
return fmt.Errorf("response body is unable to be marshalled into JSON: %w", err)
232223
}
233224
}
225+
234226
p.routesMu.Lock()
235227
defer p.routesMu.Unlock()
236228
p.routes[route.Method+":"+route.Path] = route
237229

238230
return nil
239231
}
240232

233+
// Routes returns all registered routes
234+
func (p *Server) Routes() map[string]*Route {
235+
p.routesMu.RLock()
236+
defer p.routesMu.RUnlock()
237+
return p.routes
238+
}
239+
241240
// Unregister removes a route from the parrot
242241
func (p *Server) Unregister(method, path string) {
243242
p.routesMu.Lock()
@@ -247,7 +246,7 @@ func (p *Server) Unregister(method, path string) {
247246

248247
// Call makes a request to the parrot server
249248
func (p *Server) Call(method, path string) (*http.Response, error) {
250-
req, err := http.NewRequest(method, filepath.Join(p.server.Addr, path), nil)
249+
req, err := http.NewRequest(method, "http://"+filepath.Join(p.Address(), path), nil)
251250
if err != nil {
252251
return nil, fmt.Errorf("failed to create request: %w", err)
253252
}
@@ -260,24 +259,29 @@ func (p *Server) Call(method, path string) (*http.Response, error) {
260259
func (p *Server) registerRouteHandler(w http.ResponseWriter, r *http.Request) {
261260
if r.Method != http.MethodPost {
262261
http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
262+
p.log.Trace().Str("Method", r.Method).Msg("Invalid method")
263263
return
264264
}
265265

266-
var route Route
266+
var route *Route
267267
if err := json.NewDecoder(r.Body).Decode(&route); err != nil {
268268
http.Error(w, "Invalid request body", http.StatusBadRequest)
269269
return
270270
}
271271
defer r.Body.Close()
272272

273273
if route.Method == "" || route.Path == "" {
274-
http.Error(w, "Method and Path are required", http.StatusBadRequest)
274+
err := errors.New("Method and path are required")
275+
http.Error(w, err.Error(), http.StatusBadRequest)
275276
return
276277
}
277278

278-
p.routesMu.Lock()
279-
p.routes[route.Method+":"+route.Path] = &route
280-
p.routesMu.Unlock()
279+
err := p.Register(route)
280+
if err != nil {
281+
http.Error(w, err.Error(), http.StatusBadRequest)
282+
p.log.Trace().Err(err).Msg("Failed to register route")
283+
return
284+
}
281285

282286
w.WriteHeader(http.StatusCreated)
283287
p.log.Info().Str("Path", route.Path).Str("Method", route.Method).Msg("Route registered")
@@ -291,13 +295,65 @@ func (p *Server) dynamicHandler(w http.ResponseWriter, r *http.Request) {
291295

292296
if !exists {
293297
http.NotFound(w, r)
298+
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Route not found")
294299
return
295300
}
296301

297-
w.Header().Set("Content-Type", route.ResponseContentType)
302+
if route.ResponseContentType != "" {
303+
w.Header().Set("Content-Type", route.ResponseContentType)
304+
}
298305
w.WriteHeader(route.ResponseStatusCode)
299-
if err := json.NewEncoder(w).Encode(route.ResponseBody); err != nil {
300-
http.Error(w, "Failed to marshal response into json", http.StatusInternalServerError)
306+
307+
if route.Handler != nil {
308+
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Calling route handler")
309+
route.Handler(w, r)
310+
} else if route.RawResponseBody != "" {
311+
if route.ResponseContentType == "" {
312+
w.Header().Set("Content-Type", "text/plain")
313+
}
314+
if _, err := w.Write([]byte(route.RawResponseBody)); err != nil {
315+
p.log.Trace().Err(err).Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Failed to write response")
316+
http.Error(w, "Failed to write response", http.StatusInternalServerError)
317+
return
318+
}
319+
p.log.Trace().
320+
Str("Remote Addr", r.RemoteAddr).
321+
Str("Response", route.RawResponseBody).
322+
Str("Path", r.URL.Path).
323+
Str("Method", r.Method).
324+
Msg("Returned raw response")
325+
} else if route.ResponseBody != nil {
326+
if route.ResponseContentType == "" {
327+
w.Header().Set("Content-Type", "application/json")
328+
}
329+
rawJSON, err := json.Marshal(route.ResponseBody)
330+
if err != nil {
331+
p.log.Trace().Err(err).
332+
Str("Remote Addr", r.RemoteAddr).
333+
Str("Path", r.URL.Path).
334+
Str("Method", r.Method).
335+
Msg("Failed to marshal JSON response")
336+
http.Error(w, "Failed to marshal response into json", http.StatusInternalServerError)
337+
return
338+
}
339+
if _, err = w.Write(rawJSON); err != nil {
340+
p.log.Trace().Err(err).
341+
RawJSON("Response", rawJSON).
342+
Str("Remote Addr", r.RemoteAddr).
343+
Str("Path", r.URL.Path).
344+
Str("Method", r.Method).
345+
Msg("Failed to write response")
346+
http.Error(w, "Failed to write JSON response", http.StatusInternalServerError)
347+
return
348+
}
349+
p.log.Trace().
350+
Str("Remote Addr", r.RemoteAddr).
351+
RawJSON("Response", rawJSON).
352+
Str("Path", r.URL.Path).
353+
Str("Method", r.Method).
354+
Msg("Returned JSON response")
355+
} else {
356+
p.log.Trace().Str("Remote Addr", r.RemoteAddr).Str("Path", r.URL.Path).Str("Method", r.Method).Msg("Route has no response")
301357
}
302358
}
303359

@@ -336,7 +392,7 @@ func (p *Server) save() error {
336392
p.log.Debug().Msg("No routes to save")
337393
return nil
338394
}
339-
p.log.Debug().Str("file", p.saveFile).Msg("Saving routes")
395+
p.log.Trace().Str("file", p.saveFile).Msg("Saving routes")
340396

341397
p.routesMu.RLock()
342398
defer p.routesMu.RUnlock()
@@ -350,6 +406,6 @@ func (p *Server) save() error {
350406
return fmt.Errorf("failed to write routes to file: %w", err)
351407
}
352408

353-
p.log.Info().Str("file", p.saveFile).Msg("Saved routes")
409+
p.log.Trace().Str("file", p.saveFile).Msg("Saved routes")
354410
return nil
355411
}

parrot/parrot_examples_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package parrot_test
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
8+
"github.com/smartcontractkit/chainlink-testing-framework/parrot"
9+
)
10+
11+
func ExampleServer_Register() {
12+
p, err := parrot.Wake()
13+
if err != nil {
14+
panic(err)
15+
}
16+
17+
route := &parrot.Route{
18+
Method: http.MethodGet,
19+
Path: "/test",
20+
RawResponseBody: "Squawk",
21+
ResponseStatusCode: 200,
22+
ResponseContentType: "text/plain",
23+
}
24+
25+
err = p.Register(route)
26+
if err != nil {
27+
panic(err)
28+
}
29+
30+
resp, err := p.Call(http.MethodGet, "/test")
31+
if err != nil {
32+
panic(err)
33+
}
34+
defer resp.Body.Close()
35+
36+
fmt.Println(resp.StatusCode)
37+
fmt.Println(resp.Header.Get("Content-Type"))
38+
body, _ := io.ReadAll(resp.Body)
39+
fmt.Println(string(body))
40+
41+
// Output:
42+
// 200
43+
// text/plain
44+
// Squawk
45+
}

0 commit comments

Comments
 (0)