-
Notifications
You must be signed in to change notification settings - Fork 0
/
e2e.go
280 lines (237 loc) · 6.45 KB
/
e2e.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
package e2e
import (
"bytes"
"encoding/json"
"flag"
"io"
"net/http"
"net/http/httptest"
"net/http/httputil"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
var (
router http.Handler
dumpRawResponse = flag.Bool("dump", false, "dump raw response")
updateGolden = flag.Bool("golden", false, "update golden files")
)
// RegisterRouter registers router for RunTest.
func RegisterRouter(rt http.Handler) {
router = rt
}
// ResponseFilter is a function to modify HTTP response.
type ResponseFilter func(t *testing.T, r *http.Response)
// RunTest sends an HTTP request to router, then checks the status code and
// compare the response with the golden file. When `updateGolden` is true,
// update the golden file instead of comparison.
func RunTest(t *testing.T, r *http.Request, want int, filters ...ResponseFilter) {
t.Helper()
t.Logf(">>> %s %s\n", r.Method, r.URL)
w := httptest.NewRecorder()
router.ServeHTTP(w, r)
got := w.Result()
if got.StatusCode != want {
t.Errorf("HTTP StatusCode: %d, want: %d\n", got.StatusCode, want)
}
if *dumpRawResponse {
var rc io.ReadCloser
rc, got.Body = drainBody(t, got.Body)
body, err := io.ReadAll(rc)
if err != nil {
t.Fatal(err)
}
if strings.HasPrefix(got.Header.Get("Content-Type"), "application/json") {
switch got.StatusCode {
case http.StatusOK, http.StatusCreated:
body = indentJSON(t, body)
}
}
dump, err := httputil.DumpResponse(got, false)
if err != nil {
t.Fatal(err)
}
t.Logf("Raw response:\n%s%s\n", dump, body)
}
for _, f := range filters {
f(t, got)
}
dump, err := httputil.DumpResponse(got, true)
if err != nil {
t.Fatal(err)
}
if *updateGolden {
writeGolden(t, dump)
} else {
golden := readGolden(t)
if diff := cmp.Diff(golden, dump); diff != "" {
t.Errorf("HTTP Response mismatch (-want +got):\n%s", diff)
}
}
t.Logf("<<< %s\n", goldenFileName(t.Name()))
}
// This is a modified version of httputil.drainBody for this test.
func drainBody(t *testing.T, b io.ReadCloser) (dump, orig io.ReadCloser) {
t.Helper()
if b == nil || b == http.NoBody {
return http.NoBody, http.NoBody
}
var buf bytes.Buffer
if _, err := buf.ReadFrom(b); err != nil {
t.Fatal(err)
}
_ = b.Close()
return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes()))
}
func indentJSON(t *testing.T, body []byte) []byte {
t.Helper()
var tmp map[string]any
if err := json.Unmarshal(body, &tmp); err != nil {
t.Fatal(err)
}
body, err := json.MarshalIndent(&tmp, "", " ")
if err != nil {
t.Fatal(err)
}
return body
}
func goldenFileName(name string) string {
return filepath.Join("testdata", name+".golden")
}
func writeGolden(t *testing.T, data []byte) {
t.Helper()
filename := goldenFileName(t.Name())
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filename, data, 0o600); err != nil {
t.Fatal(err)
}
}
func readGolden(t *testing.T) []byte {
t.Helper()
data, err := os.ReadFile(goldenFileName(t.Name()))
if err != nil {
t.Fatal(err)
}
return data
}
func rewriteMap(t *testing.T, base, overwrite map[string]any, parents ...string) {
t.Helper()
for k, v := range overwrite {
if old, ok := base[k]; ok {
switch v := v.(type) {
case map[string]any:
sub, ok := old.(map[string]any)
if !ok {
t.Fatalf("could not rewrite map: key = %q", strings.Join(append(parents, k), "."))
}
rewriteMap(t, sub, v, append(parents, k)...)
case []map[string]any:
sub, ok := old.([]any) // body is []any.
if !ok {
t.Fatalf("could not rewrite array map: key = %q", strings.Join(append(parents, k), "."))
}
if len(sub) != len(v) {
t.Fatalf("could not rewrite array map: len(sub)=%d != len(v)=%d: key = %q",
len(sub), len(v), strings.Join(append(parents, k), "."))
}
for i, vv := range v {
kk := k + "#" + strconv.Itoa(i)
sub2, ok := sub[i].(map[string]any)
if !ok {
t.Fatalf("could not rewrite array map: key = %q", strings.Join(append(parents, kk), "."))
}
rewriteMap(t, sub2, vv, append(parents, kk)...)
}
default:
base[k] = v
}
}
}
}
// ModifyJSON overwrites the specified key in the JSON field of the response
// body if it exists. When the map value of overwrite is map[string]any,
// change only the specified fields.
func ModifyJSON(overwrite map[string]any) ResponseFilter {
return func(t *testing.T, r *http.Response) {
t.Helper()
var tmp map[string]any
if err := json.NewDecoder(r.Body).Decode(&tmp); err != nil {
t.Fatal(err)
}
rewriteMap(t, tmp, overwrite)
body := new(bytes.Buffer)
if err := json.NewEncoder(body).Encode(&tmp); err != nil {
t.Fatal(err)
}
r.Body = io.NopCloser(body)
}
}
// PrettyJSON is a ResponseFilter for formatting JSON responses. It adds
// indentation if the status code is not 204.
func PrettyJSON(t *testing.T, r *http.Response) {
t.Helper()
if r.StatusCode == http.StatusNoContent {
return
}
if !strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
t.Fatal("Response is not JSON")
}
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
r.Body = io.NopCloser(bytes.NewReader(indentJSON(t, body)))
}
// CaptureResponse unmarshals JSON response.
func CaptureResponse[T any](ptr *T) ResponseFilter {
return func(t *testing.T, r *http.Response) {
t.Helper()
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal(err)
}
r.Body = io.NopCloser(bytes.NewReader(body))
if err := json.Unmarshal(body, &ptr); err != nil {
t.Fatal(err)
}
}
}
type RequestOption func(*http.Request)
// WithQuery sets query parameter.
func WithQuery(key string, values ...string) RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
for _, value := range values {
q.Add(key, value)
}
r.URL.RawQuery = q.Encode()
}
}
// WithHeader sets HTTP header.
func WithHeader(key, value string) RequestOption {
return func(r *http.Request) {
r.Header.Set(key, value)
}
}
// NewRequest creates a new HTTP request and applies options.
func NewRequest(method, endpoint string, body io.Reader, options ...RequestOption) *http.Request {
r := httptest.NewRequest(method, endpoint, body)
for _, opt := range options {
opt(r)
}
return r
}
// JSONBody encodes m and returns it as an io.Reader.
func JSONBody(t *testing.T, m map[string]any) io.Reader {
t.Helper()
body := new(bytes.Buffer)
if err := json.NewEncoder(body).Encode(&m); err != nil {
t.Fatal(err)
}
return body
}