Skip to content

Commit 52e167c

Browse files
authored
test: server client mock (#2571)
1 parent fae73fd commit 52e167c

File tree

275 files changed

+8834
-10259
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

275 files changed

+8834
-10259
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ require (
3535
github.com/exoscale/egoscale/v3 v3.1.13
3636
github.com/go-jose/go-jose/v4 v4.0.5
3737
github.com/go-viper/mapstructure/v2 v2.2.1
38+
github.com/google/go-cmp v0.7.0
3839
github.com/google/go-querystring v1.1.0
3940
github.com/gophercloud/gophercloud v1.14.1
4041
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package servermock
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"slices"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// Link represents a middleware interface, enabling middleware chaining.
13+
type Link interface {
14+
Bind(next http.Handler) http.Handler
15+
}
16+
17+
// LinkFunc defines a function type [Link].
18+
type LinkFunc func(next http.Handler) http.Handler
19+
20+
func (f LinkFunc) Bind(next http.Handler) http.Handler {
21+
return f(next)
22+
}
23+
24+
// ClientBuilder defines a function type for creating a client of type T based on a httptest.Server instance.
25+
type ClientBuilder[T any] func(server *httptest.Server) (T, error)
26+
27+
// Builder is a type that facilitates the construction of testable HTTP clients and server.
28+
// It allows defining routes, attaching middleware, and creating custom HTTP clients.
29+
type Builder[T any] struct {
30+
mux *http.ServeMux
31+
chain []Link
32+
33+
clientBuilder ClientBuilder[T]
34+
}
35+
36+
func NewBuilder[T any](clientBuilder ClientBuilder[T], chain ...Link) *Builder[T] {
37+
return &Builder[T]{
38+
mux: http.NewServeMux(),
39+
chain: chain,
40+
clientBuilder: clientBuilder,
41+
}
42+
}
43+
44+
func (b *Builder[T]) Route(pattern string, handler http.Handler, chain ...Link) *Builder[T] {
45+
if handler == nil {
46+
handler = Noop()
47+
}
48+
49+
for _, link := range slices.Backward(b.chain) {
50+
handler = link.Bind(handler)
51+
}
52+
53+
for _, link := range slices.Backward(chain) {
54+
handler = link.Bind(handler)
55+
}
56+
57+
b.mux.Handle(pattern, handler)
58+
59+
return b
60+
}
61+
62+
func (b *Builder[T]) Build(t *testing.T) T {
63+
t.Helper()
64+
65+
server := httptest.NewServer(b.mux)
66+
t.Cleanup(server.Close)
67+
68+
client, err := b.clientBuilder(server)
69+
require.NoError(t, err)
70+
71+
return client
72+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package servermock
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httputil"
7+
)
8+
9+
// DumpRequest logs the full HTTP request to the console, including the body if present.
10+
func DumpRequest() http.HandlerFunc {
11+
return func(rw http.ResponseWriter, req *http.Request) {
12+
dump, err := httputil.DumpRequest(req, true)
13+
if err != nil {
14+
http.Error(rw, err.Error(), http.StatusInternalServerError)
15+
return
16+
}
17+
18+
fmt.Println(string(dump))
19+
}
20+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package servermock
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"os"
7+
"path/filepath"
8+
"slices"
9+
)
10+
11+
// ResponseFromFileHandler handles HTTP responses using the content of a file.
12+
type ResponseFromFileHandler struct {
13+
statusCode int
14+
headers http.Header
15+
filename string
16+
}
17+
18+
func ResponseFromFile(filename string) *ResponseFromFileHandler {
19+
return &ResponseFromFileHandler{
20+
statusCode: http.StatusOK,
21+
headers: http.Header{},
22+
filename: filename,
23+
}
24+
}
25+
26+
func ResponseFromFixture(filename string) *ResponseFromFileHandler {
27+
return ResponseFromFile(filepath.Join("fixtures", filename))
28+
}
29+
30+
func (h *ResponseFromFileHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
31+
for k, values := range h.headers {
32+
for _, v := range values {
33+
rw.Header().Add(k, v)
34+
}
35+
}
36+
37+
if h.filename == "" {
38+
rw.WriteHeader(h.statusCode)
39+
return
40+
}
41+
42+
if filepath.Ext(h.filename) == ".json" {
43+
rw.Header().Set(contentTypeHeader, applicationJSONMimeType)
44+
}
45+
46+
file, err := os.Open(h.filename)
47+
if err != nil {
48+
http.Error(rw, err.Error(), http.StatusInternalServerError)
49+
return
50+
}
51+
52+
defer func() { _ = file.Close() }()
53+
54+
rw.WriteHeader(h.statusCode)
55+
56+
_, err = io.Copy(rw, file)
57+
if err != nil {
58+
http.Error(rw, err.Error(), http.StatusInternalServerError)
59+
return
60+
}
61+
}
62+
63+
func (h *ResponseFromFileHandler) WithStatusCode(status int) *ResponseFromFileHandler {
64+
if h.statusCode >= http.StatusContinue {
65+
h.statusCode = status
66+
}
67+
68+
return h
69+
}
70+
71+
func (h *ResponseFromFileHandler) WithHeader(name, value string, values ...string) *ResponseFromFileHandler {
72+
for _, v := range slices.Concat([]string{value}, values) {
73+
h.headers.Add(name, v)
74+
}
75+
76+
return h
77+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package servermock
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
)
7+
8+
// JSONEncodeHandler is a handler that encodes data into JSON and writes it to an HTTP response.
9+
type JSONEncodeHandler struct {
10+
data any
11+
statusCode int
12+
}
13+
14+
func JSONEncode(data any) *JSONEncodeHandler {
15+
return &JSONEncodeHandler{
16+
data: data,
17+
statusCode: http.StatusOK,
18+
}
19+
}
20+
21+
func (h *JSONEncodeHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
22+
rw.Header().Set(contentTypeHeader, applicationJSONMimeType)
23+
24+
rw.WriteHeader(h.statusCode)
25+
26+
err := json.NewEncoder(rw).Encode(h.data)
27+
if err != nil {
28+
http.Error(rw, err.Error(), http.StatusInternalServerError)
29+
return
30+
}
31+
}
32+
33+
func (h *JSONEncodeHandler) WithStatusCode(status int) *JSONEncodeHandler {
34+
if h.statusCode >= http.StatusContinue {
35+
h.statusCode = status
36+
}
37+
38+
return h
39+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package servermock
2+
3+
import (
4+
"net/http"
5+
"slices"
6+
)
7+
8+
// NoopHandler is a simple HTTP handler that responds without processing requests.
9+
type NoopHandler struct {
10+
statusCode int
11+
headers http.Header
12+
}
13+
14+
func Noop() *NoopHandler {
15+
return &NoopHandler{
16+
statusCode: http.StatusOK,
17+
headers: http.Header{},
18+
}
19+
}
20+
21+
func (h *NoopHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
22+
for k, values := range h.headers {
23+
for _, v := range values {
24+
rw.Header().Add(k, v)
25+
}
26+
}
27+
28+
rw.WriteHeader(h.statusCode)
29+
}
30+
31+
func (h *NoopHandler) WithStatusCode(status int) *NoopHandler {
32+
if h.statusCode >= http.StatusContinue {
33+
h.statusCode = status
34+
}
35+
36+
return h
37+
}
38+
39+
func (h *NoopHandler) WithHeader(name, value string, values ...string) *NoopHandler {
40+
for _, v := range slices.Concat([]string{value}, values) {
41+
h.headers.Add(name, v)
42+
}
43+
44+
return h
45+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package servermock
2+
3+
import (
4+
"net/http"
5+
"slices"
6+
)
7+
8+
// RawResponseHandler is a custom HTTP handler that serves raw response data.
9+
type RawResponseHandler struct {
10+
statusCode int
11+
headers http.Header
12+
data []byte
13+
}
14+
15+
func RawResponse(data []byte) *RawResponseHandler {
16+
return &RawResponseHandler{
17+
statusCode: http.StatusOK,
18+
headers: http.Header{},
19+
data: data,
20+
}
21+
}
22+
23+
func RawStringResponse(data string) *RawResponseHandler {
24+
return RawResponse([]byte(data))
25+
}
26+
27+
func (h *RawResponseHandler) ServeHTTP(rw http.ResponseWriter, _ *http.Request) {
28+
for k, values := range h.headers {
29+
for _, v := range values {
30+
rw.Header().Add(k, v)
31+
}
32+
}
33+
34+
rw.WriteHeader(h.statusCode)
35+
36+
if len(h.data) == 0 {
37+
return
38+
}
39+
40+
_, err := rw.Write(h.data)
41+
if err != nil {
42+
http.Error(rw, err.Error(), http.StatusInternalServerError)
43+
return
44+
}
45+
}
46+
47+
func (h *RawResponseHandler) WithStatusCode(status int) *RawResponseHandler {
48+
if h.statusCode >= http.StatusContinue {
49+
h.statusCode = status
50+
}
51+
52+
return h
53+
}
54+
55+
func (h *RawResponseHandler) WithHeader(name, value string, values ...string) *RawResponseHandler {
56+
for _, v := range slices.Concat([]string{value}, values) {
57+
h.headers.Add(name, v)
58+
}
59+
60+
return h
61+
}

0 commit comments

Comments
 (0)