Skip to content

Commit 4c9e8c1

Browse files
committed
wip template
Signed-off-by: Maxime Soulé <[email protected]>
1 parent af554f9 commit 4c9e8c1

10 files changed

+887
-159
lines changed

helpers/tdhttp/internal/any.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) 2022, 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+
//go:build !go1.18
8+
// +build !go1.18
9+
10+
package internal
11+
12+
type any = interface{}
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) 2021, 2022, 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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright (c) 2021, 2022, 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"
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: io.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+
td.Re(`Received response:
107+
"HTTP/1.0 200 OK\\r\\nA: foo\\r\\nB: bar\\r\\n\\r\\n(\\u007f|\\x7f)"`))
108+
}

helpers/tdhttp/internal/response.go

+27-56
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2021, Maxime Soulé
1+
// Copyright (c) 2022, Maxime Soulé
22
// All rights reserved.
33
//
44
// This source code is licensed under the BSD-style license found in the
@@ -7,73 +7,44 @@
77
package internal
88

99
import (
10-
"bytes"
10+
"encoding/json"
1111
"net/http"
12-
"net/http/httputil"
13-
"testing"
14-
"unicode/utf8"
12+
"net/http/httptest"
13+
"sync"
1514
)
1615

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-
}
16+
type Response struct {
17+
sync.Mutex
3818

39-
func replaceCrLf(b []byte) []byte {
40-
return bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
19+
name string
20+
response *httptest.ResponseRecorder
21+
22+
asJSON any
23+
jsonDecoded bool
4124
}
4225

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
26+
func NewResponse(resp *httptest.ResponseRecorder) *Response {
27+
return &Response{
28+
response: resp,
5429
}
55-
56-
return b, cr == 0
5730
}
5831

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()
32+
func (r *Response) Response() *http.Response {
33+
// No lock needed here
34+
return r.response.Result()
35+
}
6536

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"))
37+
func (r *Response) UnmarshalJSON() (any, error) {
38+
r.Lock()
39+
defer r.Unlock()
7040

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
41+
if !r.jsonDecoded {
42+
err := json.Unmarshal(r.response.Body.Bytes(), &r.asJSON)
43+
if err != nil {
44+
return nil, err
7545
}
46+
r.jsonDecoded = true
7647
}
7748

78-
t.Logf(label+"%q", b)
49+
return r.asJSON, nil
7950
}
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) 2022, 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+
"errors"
11+
"fmt"
12+
"sync"
13+
)
14+
15+
type ResponseList struct {
16+
sync.Mutex
17+
responses map[string]*Response
18+
last *Response
19+
}
20+
21+
func NewResponseList() *ResponseList {
22+
return &ResponseList{
23+
responses: map[string]*Response{},
24+
}
25+
}
26+
27+
func (rl *ResponseList) SetLast(resp *Response) {
28+
rl.Lock()
29+
defer rl.Unlock()
30+
31+
rl.last = resp
32+
}
33+
34+
func (rl *ResponseList) RecordLast(name string) error {
35+
rl.Lock()
36+
defer rl.Unlock()
37+
38+
if rl.last == nil {
39+
return errors.New("no last response to record")
40+
}
41+
42+
rl.last.Lock()
43+
defer rl.last.Unlock()
44+
45+
if rl.last.name != "" {
46+
return fmt.Errorf("last response is already recorded as %q", rl.last.name)
47+
}
48+
49+
rl.responses[name] = rl.last
50+
rl.last.name = name
51+
52+
return nil
53+
}
54+
55+
func (rl *ResponseList) Reset() {
56+
rl.Lock()
57+
defer rl.Unlock()
58+
59+
for name := range rl.responses {
60+
delete(rl.responses, name)
61+
}
62+
rl.last = nil
63+
}
64+
65+
func (rl *ResponseList) Get(name string) *Response {
66+
rl.Lock()
67+
defer rl.Unlock()
68+
return rl.responses[name]
69+
}
70+
71+
func (rl *ResponseList) Last() *Response {
72+
rl.Lock()
73+
defer rl.Unlock()
74+
return rl.last
75+
}

0 commit comments

Comments
 (0)