blob: b20bc7db81dd449ffe4bc8e3020f275aa11c0aee [file] [log] [blame]
Earl Lee2e463fb2025-04-17 11:22:22 -07001// Copyright 2024 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package httprr
6
7import (
8 "bytes"
9 "errors"
10 "io"
11 "net/http"
12 "net/http/httptest"
13 "os"
14 "strings"
15 "testing"
16 "testing/iotest"
17)
18
19func handler(w http.ResponseWriter, r *http.Request) {
20 if strings.HasSuffix(r.URL.Path, "/redirect") {
21 http.Error(w, "redirect me!", 304)
22 return
23 }
24 if r.Method == "GET" {
25 if r.Header.Get("Secret") != "key" {
26 http.Error(w, "missing secret", 666)
27 return
28 }
29 }
30 if r.Method == "POST" {
31 data, err := io.ReadAll(r.Body)
32 if err != nil {
33 panic(err)
34 }
35 if !strings.Contains(string(data), "my Secret") {
36 http.Error(w, "missing body secret", 667)
37 return
38 }
39 }
40}
41
42func always555(w http.ResponseWriter, r *http.Request) {
43 http.Error(w, "should not be making HTTP requests", 555)
44}
45
46func dropPort(r *http.Request) error {
47 if r.URL.Port() != "" {
48 r.URL.Host = r.URL.Host[:strings.LastIndex(r.URL.Host, ":")]
49 r.Host = r.Host[:strings.LastIndex(r.Host, ":")]
50 }
51 return nil
52}
53
54func dropSecretHeader(r *http.Request) error {
55 r.Header.Del("Secret")
56 return nil
57}
58
59func hideSecretBody(r *http.Request) error {
60 if r.Body != nil {
61 body := r.Body.(*Body)
62 body.Data = []byte("redacted")
63 }
64 return nil
65}
66
67func doNothing(b *bytes.Buffer) error {
68 return nil
69}
70
71func doRefresh(b *bytes.Buffer) error {
72 s := b.String()
73 b.Reset()
74 _, _ = b.WriteString(s)
75 return nil
76}
77
78func TestRecordReplay(t *testing.T) {
79 dir := t.TempDir()
80 file := dir + "/rr"
81
82 // 4 passes:
83 // 0: create
84 // 1: open
85 // 2: Open with -httprecord="r+"
86 // 3: Open with -httprecord=""
87 for pass := range 4 {
88 start := open
89 h := always555
90 *record = ""
91 switch pass {
92 case 0:
93 start = create
94 h = handler
95 case 2:
96 start = Open
97 *record = "r+"
98 h = handler
99 case 3:
100 start = Open
101 }
102 rr, err := start(file, http.DefaultTransport)
103 if err != nil {
104 t.Fatal(err)
105 }
106 if rr.Recording() {
107 t.Log("RECORDING")
108 } else {
109 t.Log("REPLAYING")
110 }
111 rr.ScrubReq(dropPort, dropSecretHeader)
112 rr.ScrubReq(hideSecretBody)
113 rr.ScrubResp(doNothing, doRefresh)
114
115 mustNewRequest := func(method, url string, body io.Reader) *http.Request {
116 req, err := http.NewRequest(method, url, body)
117 if err != nil {
118 t.Helper()
119 t.Fatal(err)
120 }
121 return req
122 }
123
124 mustDo := func(req *http.Request, status int) {
125 resp, err := rr.Client().Do(req)
126 if err != nil {
127 t.Helper()
128 t.Fatal(err)
129 }
130 body, _ := io.ReadAll(resp.Body)
131 resp.Body.Close()
132 if resp.StatusCode != status {
133 t.Helper()
134 t.Fatalf("%v: %s\n%s", req.URL, resp.Status, body)
135 }
136 }
137
138 srv := httptest.NewServer(http.HandlerFunc(h))
139 defer srv.Close()
140
141 req := mustNewRequest("GET", srv.URL+"/myrequest", nil)
142 req.Header.Set("Secret", "key")
143 mustDo(req, 200)
144
145 req = mustNewRequest("POST", srv.URL+"/myrequest", strings.NewReader("my Secret"))
146 mustDo(req, 200)
147
148 req = mustNewRequest("GET", srv.URL+"/redirect", nil)
149 mustDo(req, 304)
150
151 if !rr.Recording() {
152 req = mustNewRequest("GET", srv.URL+"/uncached", nil)
153 resp, err := rr.Client().Do(req)
154 if err == nil {
155 body, _ := io.ReadAll(resp.Body)
156 t.Fatalf("%v: %s\n%s", req.URL, resp.Status, body)
157 }
158 }
159
160 if err := rr.Close(); err != nil {
161 t.Fatal(err)
162 }
163 }
164
165 data, err := os.ReadFile(file)
166 if err != nil {
167 t.Fatal(err)
168 }
169 if strings.Contains(string(data), "Secret") {
170 t.Fatalf("rr file contains Secret:\n%s", data)
171 }
172}
173
174var badResponseTrace = []byte("httprr trace v1\n" +
175 "92 75\n" +
176 "GET http://127.0.0.1/myrequest HTTP/1.1\r\n" +
177 "Host: 127.0.0.1\r\n" +
178 "User-Agent: Go-http-client/1.1\r\n" +
179 "\r\n" +
180 "HZZP/1.1 200 OK\r\n" +
181 "Date: Wed, 12 Jun 2024 13:55:02 GMT\r\n" +
182 "Content-Length: 0\r\n" +
183 "\r\n")
184
185func TestErrors(t *testing.T) {
186 dir := t.TempDir()
187
188 makeTmpFile := func() string {
189 f, err := os.CreateTemp(dir, "TestErrors")
190 if err != nil {
191 t.Fatalf("failed to create tmp file for test: %v", err)
192 }
193 name := f.Name()
194 f.Close()
195 return name
196 }
197
198 // -httprecord regexp parsing
199 *record = "+"
200 if _, err := Open(makeTmpFile(), nil); err == nil || !strings.Contains(err.Error(), "invalid -httprecord flag") {
201 t.Errorf("did not diagnose bad -httprecord: err = %v", err)
202 }
203 *record = ""
204
205 // invalid httprr trace
206 if _, err := Open(makeTmpFile(), nil); err == nil || !strings.Contains(err.Error(), "not an httprr trace") {
207 t.Errorf("did not diagnose invalid httprr trace: err = %v", err)
208 }
209
210 // corrupt httprr trace
211 corruptTraceFile := makeTmpFile()
212 os.WriteFile(corruptTraceFile, []byte("httprr trace v1\ngarbage\n"), 0o666)
213 if _, err := Open(corruptTraceFile, nil); err == nil || !strings.Contains(err.Error(), "corrupt httprr trace") {
214 t.Errorf("did not diagnose invalid httprr trace: err = %v", err)
215 }
216
217 // os.Create error creating trace
218 if _, err := create("invalid\x00file", nil); err == nil {
219 t.Errorf("did not report failure from os.Create: err = %v", err)
220 }
221
222 // os.ReadAll error reading trace
223 if _, err := open("nonexistent", nil); err == nil {
224 t.Errorf("did not report failure from os.ReadFile: err = %v", err)
225 }
226
227 // error reading body
228 rr, err := create(makeTmpFile(), nil)
229 if err != nil {
230 t.Fatal(err)
231 }
232 if _, err := rr.Client().Post("http://127.0.0.1/nonexist", "x/error", iotest.ErrReader(errors.New("MY ERROR"))); err == nil || !strings.Contains(err.Error(), "MY ERROR") {
233 t.Errorf("did not report failure from io.ReadAll(body): err = %v", err)
234 }
235
236 // error during request scrub
237 rr.ScrubReq(func(*http.Request) error { return errors.New("SCRUB ERROR") })
238 if _, err := rr.Client().Get("http://127.0.0.1/nonexist"); err == nil || !strings.Contains(err.Error(), "SCRUB ERROR") {
239 t.Errorf("did not report failure from scrub: err = %v", err)
240 }
241 rr.Close()
242
243 // error during response scrub
244 rr.ScrubResp(func(*bytes.Buffer) error { return errors.New("SCRUB ERROR") })
245 if _, err := rr.Client().Get("http://127.0.0.1/nonexist"); err == nil || !strings.Contains(err.Error(), "SCRUB ERROR") {
246 t.Errorf("did not report failure from scrub: err = %v", err)
247 }
248 rr.Close()
249
250 // error during rkey.WriteProxy
251 rr, err = create(makeTmpFile(), nil)
252 if err != nil {
253 t.Fatal(err)
254 }
255 rr.ScrubReq(func(req *http.Request) error {
256 req.URL = nil
257 req.Host = ""
258 return nil
259 })
260 rr.ScrubResp(func(b *bytes.Buffer) error {
261 b.Reset()
262 return nil
263 })
264 if _, err := rr.Client().Get("http://127.0.0.1/nonexist"); err == nil || !strings.Contains(err.Error(), "no Host or URL set") {
265 t.Errorf("did not report failure from rkey.WriteProxy: err = %v", err)
266 }
267 rr.Close()
268
269 // error during resp.Write
270 rr, err = create(makeTmpFile(), badRespTransport{})
271 if err != nil {
272 t.Fatal(err)
273 }
274 if _, err := rr.Client().Get("http://127.0.0.1/nonexist"); err == nil || !strings.Contains(err.Error(), "TRANSPORT ERROR") {
275 t.Errorf("did not report failure from resp.Write: err = %v", err)
276 }
277 rr.Close()
278
279 // error during Write logging request
280 srv := httptest.NewServer(http.HandlerFunc(always555))
281 defer srv.Close()
282 rr, err = create(makeTmpFile(), http.DefaultTransport)
283 if err != nil {
284 t.Fatal(err)
285 }
286 rr.ScrubReq(dropPort)
287 rr.record.Close() // cause write error
288 if _, err := rr.Client().Get(srv.URL + "/redirect"); err == nil || !strings.Contains(err.Error(), "file already closed") {
289 t.Errorf("did not report failure from record write: err = %v", err)
290 }
291 rr.writeErr = errors.New("BROKEN ERROR")
292 if _, err := rr.Client().Get(srv.URL + "/redirect"); err == nil || !strings.Contains(err.Error(), "BROKEN ERROR") {
293 t.Errorf("did not report previous write failure: err = %v", err)
294 }
295 if err := rr.Close(); err == nil || !strings.Contains(err.Error(), "BROKEN ERROR") {
296 t.Errorf("did not report write failure during close: err = %v", err)
297 }
298
299 // error during RoundTrip
300 rr, err = create(makeTmpFile(), errTransport{errors.New("TRANSPORT ERROR")})
301 if err != nil {
302 t.Fatal(err)
303 }
304 if _, err := rr.Client().Get(srv.URL); err == nil || !strings.Contains(err.Error(), "TRANSPORT ERROR") {
305 t.Errorf("did not report failure from transport: err = %v", err)
306 }
307 rr.Close()
308
309 // error during http.ReadResponse: trace is structurally okay but has malformed response inside
310 tmpFile := makeTmpFile()
311 if err := os.WriteFile(tmpFile, badResponseTrace, 0o666); err != nil {
312 t.Fatal(err)
313 }
314 rr, err = Open(tmpFile, nil)
315 if err != nil {
316 t.Fatal(err)
317 }
318 if _, err := rr.Client().Get("http://127.0.0.1/myrequest"); err == nil || !strings.Contains(err.Error(), "corrupt httprr trace:") {
319 t.Errorf("did not diagnose invalid httprr trace: err = %v", err)
320 }
321 rr.Close()
322}
323
324type errTransport struct{ err error }
325
326func (e errTransport) RoundTrip(req *http.Request) (*http.Response, error) {
327 return nil, e.err
328}
329
330type badRespTransport struct{}
331
332func (badRespTransport) RoundTrip(req *http.Request) (*http.Response, error) {
333 resp := new(http.Response)
334 resp.Body = io.NopCloser(iotest.ErrReader(errors.New("TRANSPORT ERROR")))
335 return resp, nil
336}