Skip to content

Commit 98371d6

Browse files
committed
Add 'pkg/' from commit 'b1a27bff57094e3803e4106453021b444b104ba8'
git-subtree-dir: pkg git-subtree-mainline: 4ee2f85 git-subtree-split: b1a27bf
2 parents 4ee2f85 + b1a27bf commit 98371d6

File tree

2 files changed

+262
-0
lines changed

2 files changed

+262
-0
lines changed

pkg/request_helpers.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
// Copyright 2019 DeepMap, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
package testutil
15+
16+
// This is a set of fluent request builders for tests, which help us to
17+
// simplify constructing and unmarshaling test objects. For example, to post
18+
// a body and return a response, you would do something like:
19+
//
20+
// var body RequestBody
21+
// var response ResponseBody
22+
// t is *testing.T, from a unit test
23+
// e is *echo.Echo
24+
// response := NewRequest().Post("/path").WithJsonBody(body).Go(t, e)
25+
// err := response.UnmarshalBodyToObject(&response)
26+
import (
27+
"bytes"
28+
"encoding/json"
29+
"fmt"
30+
"io"
31+
"net/http"
32+
"net/http/httptest"
33+
"strings"
34+
"testing"
35+
36+
"github.com/labstack/echo/v4"
37+
)
38+
39+
func NewRequest() *RequestBuilder {
40+
return &RequestBuilder{
41+
Headers: make(map[string]string),
42+
}
43+
}
44+
45+
// RequestBuilder caches request settings as we build up the request.
46+
type RequestBuilder struct {
47+
Method string
48+
Path string
49+
Headers map[string]string
50+
Body []byte
51+
Error error
52+
Cookies []*http.Cookie
53+
}
54+
55+
// WithMethod sets the method and path
56+
func (r *RequestBuilder) WithMethod(method string, path string) *RequestBuilder {
57+
r.Method = method
58+
r.Path = path
59+
return r
60+
}
61+
62+
func (r *RequestBuilder) Get(path string) *RequestBuilder {
63+
return r.WithMethod("GET", path)
64+
}
65+
66+
func (r *RequestBuilder) Post(path string) *RequestBuilder {
67+
return r.WithMethod("POST", path)
68+
}
69+
70+
func (r *RequestBuilder) Put(path string) *RequestBuilder {
71+
return r.WithMethod("PUT", path)
72+
}
73+
74+
func (r *RequestBuilder) Patch(path string) *RequestBuilder {
75+
return r.WithMethod("PATCH", path)
76+
}
77+
78+
func (r *RequestBuilder) Delete(path string) *RequestBuilder {
79+
return r.WithMethod("DELETE", path)
80+
}
81+
82+
// WithHeader sets a header
83+
func (r *RequestBuilder) WithHeader(header, value string) *RequestBuilder {
84+
r.Headers[header] = value
85+
return r
86+
}
87+
88+
func (r *RequestBuilder) WithJWSAuth(jws string) *RequestBuilder {
89+
r.Headers["Authorization"] = "Bearer " + jws
90+
return r
91+
}
92+
93+
func (r *RequestBuilder) WithHost(value string) *RequestBuilder {
94+
return r.WithHeader("Host", value)
95+
}
96+
97+
func (r *RequestBuilder) WithContentType(value string) *RequestBuilder {
98+
return r.WithHeader("Content-Type", value)
99+
}
100+
101+
func (r *RequestBuilder) WithJsonContentType() *RequestBuilder {
102+
return r.WithContentType("application/json")
103+
}
104+
105+
func (r *RequestBuilder) WithAccept(value string) *RequestBuilder {
106+
return r.WithHeader("Accept", value)
107+
}
108+
109+
func (r *RequestBuilder) WithAcceptJson() *RequestBuilder {
110+
return r.WithAccept("application/json")
111+
}
112+
113+
// Request body operations
114+
115+
func (r *RequestBuilder) WithBody(body []byte) *RequestBuilder {
116+
r.Body = body
117+
return r
118+
}
119+
120+
// WithJsonBody takes an object as input, marshals it to JSON, and sends it
121+
// as the body with Content-Type: application/json
122+
func (r *RequestBuilder) WithJsonBody(obj interface{}) *RequestBuilder {
123+
var err error
124+
r.Body, err = json.Marshal(obj)
125+
if err != nil {
126+
r.Error = fmt.Errorf("failed to marshal json object: %w", err)
127+
}
128+
return r.WithJsonContentType()
129+
}
130+
131+
// WithCookie sets a cookie
132+
func (r *RequestBuilder) WithCookie(c *http.Cookie) *RequestBuilder {
133+
r.Cookies = append(r.Cookies, c)
134+
return r
135+
}
136+
137+
func (r *RequestBuilder) WithCookieNameValue(name, value string) *RequestBuilder {
138+
return r.WithCookie(&http.Cookie{Name: name, Value: value})
139+
}
140+
141+
// GoWithHTTPHandler performs the request, it takes a pointer to a testing context
142+
// to print messages, and a http handler for request handling.
143+
func (r *RequestBuilder) GoWithHTTPHandler(t *testing.T, handler http.Handler) *CompletedRequest {
144+
if r.Error != nil {
145+
// Fail the test if we had an error
146+
t.Errorf("error constructing request: %s", r.Error)
147+
return nil
148+
}
149+
var bodyReader io.Reader
150+
if r.Body != nil {
151+
bodyReader = bytes.NewReader(r.Body)
152+
}
153+
154+
req := httptest.NewRequest(r.Method, r.Path, bodyReader)
155+
for h, v := range r.Headers {
156+
req.Header.Add(h, v)
157+
}
158+
if host, ok := r.Headers["Host"]; ok {
159+
req.Host = host
160+
}
161+
for _, c := range r.Cookies {
162+
req.AddCookie(c)
163+
}
164+
165+
rec := httptest.NewRecorder()
166+
handler.ServeHTTP(rec, req)
167+
168+
return &CompletedRequest{
169+
Recorder: rec,
170+
}
171+
}
172+
173+
// Go performs the request, it takes a pointer to a testing context
174+
// to print messages, and a pointer to an echo context for request handling.
175+
func (r *RequestBuilder) Go(t *testing.T, e *echo.Echo) *CompletedRequest {
176+
return r.GoWithHTTPHandler(t, e)
177+
}
178+
179+
// CompletedRequest is the result of calling Go() on the request builder. We're wrapping the
180+
// ResponseRecorder with some nice helper functions.
181+
type CompletedRequest struct {
182+
Recorder *httptest.ResponseRecorder
183+
184+
// When set to true, decoders will be more strict. In the default JSON
185+
// recorder, unknown fields will cause errors.
186+
Strict bool
187+
}
188+
189+
func (c *CompletedRequest) DisallowUnknownFields() {
190+
c.Strict = true
191+
}
192+
193+
// UnmarshalBodyToObject takes a destination object as input, and unmarshals the object
194+
// in the response based on the Content-Type header.
195+
func (c *CompletedRequest) UnmarshalBodyToObject(obj interface{}) error {
196+
ctype := c.Recorder.Header().Get("Content-Type")
197+
198+
// Content type can have an annotation after ;
199+
contentParts := strings.Split(ctype, ";")
200+
content := strings.TrimSpace(contentParts[0])
201+
handler := getHandler(content)
202+
if handler == nil {
203+
return fmt.Errorf("unhandled content: %s", content)
204+
}
205+
206+
return handler(ctype, c.Recorder.Body, obj, c.Strict)
207+
}
208+
209+
// UnmarshalJsonToObject assumes that the response contains JSON and unmarshals it
210+
// into the specified object.
211+
func (c *CompletedRequest) UnmarshalJsonToObject(obj interface{}) error {
212+
return json.Unmarshal(c.Recorder.Body.Bytes(), obj)
213+
}
214+
215+
// Code is a shortcut for response code
216+
func (c *CompletedRequest) Code() int {
217+
return c.Recorder.Code
218+
}

pkg/response_handlers.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package testutil
2+
3+
import (
4+
"encoding/json"
5+
"io"
6+
"sync"
7+
)
8+
9+
func init() {
10+
knownHandlers = make(map[string]ResponseHandler)
11+
12+
RegisterResponseHandler("application/json", jsonHandler)
13+
}
14+
15+
var (
16+
knownHandlersMu sync.Mutex
17+
knownHandlers map[string]ResponseHandler
18+
)
19+
20+
type ResponseHandler func(contentType string, raw io.Reader, obj interface{}, strict bool) error
21+
22+
func RegisterResponseHandler(mime string, handler ResponseHandler) {
23+
knownHandlersMu.Lock()
24+
defer knownHandlersMu.Unlock()
25+
26+
knownHandlers[mime] = handler
27+
}
28+
29+
func getHandler(mime string) ResponseHandler {
30+
knownHandlersMu.Lock()
31+
defer knownHandlersMu.Unlock()
32+
33+
return knownHandlers[mime]
34+
}
35+
36+
// jsonHandler assumes that the response contains JSON and unmarshals it
37+
// into the specified object.
38+
func jsonHandler(_ string, r io.Reader, obj interface{}, strict bool) error {
39+
d := json.NewDecoder(r)
40+
if strict {
41+
d.DisallowUnknownFields()
42+
}
43+
return d.Decode(obj)
44+
}

0 commit comments

Comments
 (0)