Skip to content

Commit d7d9068

Browse files
authored
Merge pull request #126 from maxatome/tdhttp
feat(tdhttp): add With, T, Run, OrDumpResponse and AutoDumpResponse
2 parents c715a91 + bff4123 commit d7d9068

5 files changed

Lines changed: 517 additions & 27 deletions

File tree

helpers/tdhttp/http_test.go

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,12 @@ func TestCmpResponse(tt *testing.T) {
119119
\s+Response.Status: values differ
120120
\s+got: 242
121121
\s+expected: 243`,
122-
" Raw received body: `text response\n", // check the complete body is shown
122+
`~ Received response:
123+
\s+\x60(?s:.+?)
124+
\s+
125+
\s+text response
126+
\s+\x60
127+
`, // check the complete body is shown
123128
},
124129
},
125130
{
@@ -142,10 +147,15 @@ func TestCmpResponse(tt *testing.T) {
142147
ExpectedResp: tdhttp.Response{},
143148
ExpectedLogs: []string{
144149
`~ Failed test 'body should be empty'
145-
\s+Response.Body: not empty
146-
\s+got: (?s:.*?)
150+
\s+Response.Body is not empty
151+
\s+got: not empty
147152
\s+expected: empty`,
148-
"text response", // check the response is shown (in got:)
153+
`~ Received response:
154+
\s+\x60(?s:.+?)
155+
\s+
156+
\s+text response
157+
\s+\x60
158+
`, // check the complete body is shown
149159
},
150160
},
151161
{
@@ -173,7 +183,12 @@ func TestCmpResponse(tt *testing.T) {
173183
\s+unmarshal\(Response\.Body\): should NOT be an error
174184
\s+got: .*Cmp(Response|Body) only accepts expected(Resp\.)?Body be a \[\]byte, a string or a TestDeep operator allowing to match these types, but not type int.*
175185
\s+expected: nil`,
176-
" Raw received body: `text response\n", // check the complete body is shown
186+
`~ Received response:
187+
\s+\x60(?s:.+?)
188+
\s+
189+
\s+text response
190+
\s+\x60
191+
`, // check the complete body is shown
177192
},
178193
},
179194
// Empty success
@@ -261,7 +276,12 @@ func TestCmpJSONResponse(tt *testing.T) {
261276
\s+unmarshal\(Response\.Body\): should NOT be an error
262277
\s+got: .*cannot unmarshal object into Go value of type int.*
263278
\s+expected: nil`,
264-
`~ Raw received body: \x60\{"name":"Bob"\}\n\s+\x60`, // check the complete body is shown
279+
`~ Received response:
280+
\s+\x60(?s:.+?)
281+
\s+
282+
\s+\{"name":"Bob"\}
283+
\s+\x60
284+
`, // check the complete body is shown
265285
},
266286
},
267287
} {
@@ -405,7 +425,12 @@ func TestCmpXMLResponse(tt *testing.T) {
405425
\s+unmarshal\(Response\.Body\): should NOT be an error
406426
\s+got: .*unknown type func\(\).*
407427
\s+expected: nil`,
408-
`~ Raw received body: \x60<XResp><name>Bob</name></XResp>\n\s+\x60`, // check the complete body is shown
428+
`~ Received response:
429+
\s+\x60(?s:.+?)
430+
\s+
431+
\s+<XResp><name>Bob</name></XResp>
432+
\s+\x60
433+
`, // check the complete body is shown
409434
},
410435
},
411436
} {
@@ -421,6 +446,12 @@ func TestCmpXMLResponse(tt *testing.T) {
421446
}
422447
}
423448

449+
var logsViz = strings.NewReplacer(
450+
" ", "·",
451+
"\t", "→",
452+
"\r", "<cr>",
453+
)
454+
424455
func testLogs(t *td.T, mockT *tdutil.T, curTest CmpResponseTest) {
425456
t.Helper()
426457

@@ -440,7 +471,7 @@ func testLogs(t *td.T, mockT *tdutil.T, curTest CmpResponseTest) {
440471
}
441472

442473
if dumpLogs {
443-
t.Errorf(`Test logs: "%s"`, mockT.LogBuf())
474+
t.Errorf(`Test logs: "%s"`, logsViz.Replace(mockT.LogBuf()))
444475
}
445476
}
446477

@@ -637,7 +668,7 @@ func TestMux(t *testing.T) {
637668
t.False(ok)
638669
t.True(mockT.Failed())
639670
t.Contains(mockT.LogBuf(), "nothing has been set during unmarshaling")
640-
t.Contains(mockT.LogBuf(), "Raw received body:")
671+
t.Contains(mockT.LogBuf(), "Received response:")
641672
})
642673
})
643674

@@ -713,7 +744,7 @@ func TestMux(t *testing.T) {
713744
"Cannot guess the body expected type as Any TestDeep")
714745
t.Contains(mockT.LogBuf(),
715746
"You can try All(Isa(EXPECTED_TYPE), Any(…)) to disambiguate…")
716-
t.Contains(mockT.LogBuf(), "Raw received body:")
747+
t.Contains(mockT.LogBuf(), "Received response:")
717748
})
718749
})
719750
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2021, Maxime Soulé
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
package internal
8+
9+
import (
10+
"bytes"
11+
"net/http"
12+
"net/http/httputil"
13+
"testing"
14+
"unicode/utf8"
15+
)
16+
17+
// canBackquote is the same as strconv.CanBackquote but works on
18+
// []byte and accepts '\n' and '\r'.
19+
func canBackquote(b []byte) bool {
20+
for len(b) > 0 {
21+
r, wid := utf8.DecodeRune(b)
22+
b = b[wid:]
23+
if wid > 1 {
24+
if r == '\ufeff' {
25+
return false // BOMs are invisible and should not be quoted.
26+
}
27+
continue // All other multibyte runes are correctly encoded and assumed printable.
28+
}
29+
if r == utf8.RuneError {
30+
return false
31+
}
32+
if (r < ' ' && r != '\t' && r != '\n' && r != '\r') || r == '`' || r == '\u007F' {
33+
return false
34+
}
35+
}
36+
return true
37+
}
38+
39+
func replaceCrLf(b []byte) []byte {
40+
return bytes.Replace(b, []byte("\r\n"), []byte("\n"), -1) //nolint: gocritic
41+
}
42+
43+
func backquote(b []byte) ([]byte, bool) {
44+
// if there is as many \r\n as \n, replace all occurrences by \n
45+
// so we can conveniently print the buffer inside `…`.
46+
crnl := bytes.Count(b, []byte("\r\n"))
47+
cr := bytes.Count(b, []byte("\r"))
48+
if crnl != 0 {
49+
nl := bytes.Count(b, []byte("\n"))
50+
if crnl != nl || crnl != cr {
51+
return nil, false
52+
}
53+
return replaceCrLf(b), true
54+
}
55+
56+
return b, cr == 0
57+
}
58+
59+
// DumpResponse logs "resp" using Logf method of "t".
60+
//
61+
// It tries to produce a result as readable as possible first using
62+
// backquotes then falling back to double-quotes.
63+
func DumpResponse(t testing.TB, resp *http.Response) {
64+
t.Helper()
65+
66+
const label = "Received response:\n"
67+
b, _ := httputil.DumpResponse(resp, true)
68+
if canBackquote(b) {
69+
bodyPos := bytes.Index(b, []byte("\r\n\r\n"))
70+
71+
if body, ok := backquote(b[bodyPos+4:]); ok {
72+
headers := replaceCrLf(b[:bodyPos])
73+
t.Logf(label+"`%s\n\n%s`", headers, body)
74+
return
75+
}
76+
}
77+
78+
t.Logf(label+"%q", b)
79+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) 2021, Maxime Soulé
2+
// All rights reserved.
3+
//
4+
// This source code is licensed under the BSD-style license found in the
5+
// LICENSE file in the root directory of this source tree.
6+
7+
package internal_test
8+
9+
import (
10+
"bytes"
11+
"io/ioutil"
12+
"net/http"
13+
"testing"
14+
15+
"github.com/maxatome/go-testdeep/helpers/tdhttp/internal"
16+
"github.com/maxatome/go-testdeep/internal/test"
17+
"github.com/maxatome/go-testdeep/td"
18+
)
19+
20+
func newResponse(body string) *http.Response {
21+
return &http.Response{
22+
Status: "200 OK",
23+
StatusCode: 200,
24+
Proto: "HTTP/1.0",
25+
ProtoMajor: 1,
26+
ProtoMinor: 0,
27+
Header: http.Header{
28+
"A": []string{"foo"},
29+
"B": []string{"bar"},
30+
},
31+
Body: ioutil.NopCloser(bytes.NewBufferString(body)),
32+
}
33+
}
34+
35+
func inBQ(s string) string {
36+
return "`" + s + "`"
37+
}
38+
39+
func TestDumpResponse(t *testing.T) {
40+
tb := test.NewTestingTB("TestDumpResponse")
41+
internal.DumpResponse(tb, newResponse("one-line"))
42+
td.Cmp(t, tb.LastMessage(),
43+
`Received response:
44+
`+inBQ(`HTTP/1.0 200 OK
45+
A: foo
46+
B: bar
47+
48+
one-line`))
49+
50+
tb.ResetMessages()
51+
internal.DumpResponse(tb, newResponse("multi\r\nlines\r\nand\ttabs héhé"))
52+
td.Cmp(t, tb.LastMessage(),
53+
`Received response:
54+
`+inBQ(`HTTP/1.0 200 OK
55+
A: foo
56+
B: bar
57+
58+
multi
59+
lines
60+
`+"and\ttabs héhé"))
61+
62+
tb.ResetMessages()
63+
internal.DumpResponse(tb, newResponse("multi\nlines\nand\ttabs héhé"))
64+
td.Cmp(t, tb.LastMessage(),
65+
`Received response:
66+
`+inBQ(`HTTP/1.0 200 OK
67+
A: foo
68+
B: bar
69+
70+
multi
71+
lines
72+
`+"and\ttabs héhé"))
73+
74+
// one \r more in body
75+
tb.ResetMessages()
76+
internal.DumpResponse(tb, newResponse("multi\r\nline\r"))
77+
td.Cmp(t, tb.LastMessage(),
78+
`Received response:
79+
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\nmulti\r\nline\r"`)
80+
81+
// BOM
82+
tb.ResetMessages()
83+
internal.DumpResponse(tb, newResponse("\ufeff"))
84+
td.Cmp(t, tb.LastMessage(),
85+
`Received response:
86+
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\ufeff"`)
87+
88+
// Rune error
89+
tb.ResetMessages()
90+
internal.DumpResponse(tb, newResponse("\xf4\x9f\xbf\xbf"))
91+
td.Cmp(t, tb.LastMessage(),
92+
`Received response:
93+
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\xf4\x9f\xbf\xbf"`)
94+
95+
// `
96+
tb.ResetMessages()
97+
internal.DumpResponse(tb, newResponse("he`o"))
98+
td.Cmp(t, tb.LastMessage(),
99+
`Received response:
100+
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\nhe`+"`"+`o"`)
101+
102+
// 0x7f
103+
tb.ResetMessages()
104+
internal.DumpResponse(tb, newResponse("\x7f"))
105+
td.Cmp(t, tb.LastMessage(),
106+
`Received response:
107+
"HTTP/1.0 200 OK\r\nA: foo\r\nB: bar\r\n\r\n\u007f"`)
108+
}

0 commit comments

Comments
 (0)