depot/packages/networking/ipfs-cluster/api/common/test/helpers.go

296 lines
8.4 KiB
Go

// Package test provides utility methods to test APIs based on the common
// API.
package test
import (
"bytes"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"reflect"
"strings"
"testing"
"github.com/libp2p/go-libp2p"
p2phttp "github.com/libp2p/go-libp2p-http"
"github.com/libp2p/go-libp2p/core/host"
peerstore "github.com/libp2p/go-libp2p/core/peerstore"
)
var (
// SSLCertFile is the location of the certificate file.
// Used in HTTPClient to set the right certificate when
// creating an HTTPs client. Might need adjusting depending
// on where the tests are running.
SSLCertFile = "test/server.crt"
// ClientOrigin sets the Origin header for requests to this.
ClientOrigin = "myorigin"
)
// ProcessResp puts a response into a given type or fails the test.
func ProcessResp(t *testing.T, httpResp *http.Response, err error, resp interface{}) {
if err != nil {
t.Fatal("error making request: ", err)
}
body, err := io.ReadAll(httpResp.Body)
defer httpResp.Body.Close()
if err != nil {
t.Fatal("error reading body: ", err)
}
if len(body) != 0 {
err = json.Unmarshal(body, resp)
if err != nil {
t.Error(string(body))
t.Fatal("error parsing json: ", err)
}
}
}
// ProcessStreamingResp decodes a streaming response into the given type
// and fails the test on error.
func ProcessStreamingResp(t *testing.T, httpResp *http.Response, err error, resp interface{}, trailerError bool) {
if err != nil {
t.Fatal("error making streaming request: ", err)
}
if httpResp.StatusCode > 399 {
// normal response with error
ProcessResp(t, httpResp, err, resp)
return
}
defer httpResp.Body.Close()
dec := json.NewDecoder(httpResp.Body)
// If we passed a slice we fill it in, otherwise we just decode
// on top of the passed value.
tResp := reflect.TypeOf(resp)
if tResp.Elem().Kind() == reflect.Slice {
vSlice := reflect.MakeSlice(reflect.TypeOf(resp).Elem(), 0, 1000)
vType := tResp.Elem().Elem()
for {
v := reflect.New(vType)
err := dec.Decode(v.Interface())
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
vSlice = reflect.Append(vSlice, v.Elem())
}
reflect.ValueOf(resp).Elem().Set(vSlice)
} else {
for {
err := dec.Decode(resp)
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
}
}
trailerValues := httpResp.Trailer.Values("X-Stream-Error")
if trailerError && len(trailerValues) <= 1 && trailerValues[0] == "" {
t.Error("expected trailer error")
}
if !trailerError && len(trailerValues) >= 2 {
t.Error("got trailer error: ", trailerValues)
}
}
// CheckHeaders checks that all the headers are set to what is expected.
func CheckHeaders(t *testing.T, expected map[string][]string, url string, headers http.Header) {
for k, v := range expected {
if strings.Join(v, ",") != strings.Join(headers[k], ",") {
t.Errorf("%s does not show configured headers: %s", url, k)
}
}
if headers.Get("Content-Type") != "application/json" {
t.Errorf("%s is not application/json", url)
}
if eh := headers.Get("Access-Control-Expose-Headers"); eh == "" {
t.Error("AC-Expose-Headers not set")
}
}
// API represents what an API is to us.
type API interface {
HTTPAddresses() ([]string, error)
Host() host.Host
Headers() map[string][]string
}
// URLFunc is a function that given an API returns a url string.
type URLFunc func(a API) string
// HTTPURL returns the http endpoint of the API.
func HTTPURL(a API) string {
u, _ := a.HTTPAddresses()
return fmt.Sprintf("http://%s", u[0])
}
// P2pURL returns the libp2p endpoint of the API.
func P2pURL(a API) string {
return fmt.Sprintf("libp2p://%s", a.Host().ID().String())
}
// HttpsURL returns the HTTPS endpoint of the API
func httpsURL(a API) string {
u, _ := a.HTTPAddresses()
return fmt.Sprintf("https://%s", u[0])
}
// IsHTTPS returns true if a url string uses HTTPS.
func IsHTTPS(url string) bool {
return strings.HasPrefix(url, "https")
}
// HTTPClient returns a client that supporst both http/https and
// libp2p-tunneled-http.
func HTTPClient(t *testing.T, h host.Host, isHTTPS bool) *http.Client {
tr := &http.Transport{}
if isHTTPS {
certpool := x509.NewCertPool()
cert, err := os.ReadFile(SSLCertFile)
if err != nil {
t.Fatal("error reading cert for https client: ", err)
}
certpool.AppendCertsFromPEM(cert)
tr = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: certpool,
}}
}
if h != nil {
tr.RegisterProtocol("libp2p", p2phttp.NewTransport(h))
}
return &http.Client{Transport: tr}
}
// MakeHost makes a libp2p host that knows how to talk to the given API.
func MakeHost(t *testing.T, api API) host.Host {
h, err := libp2p.New()
if err != nil {
t.Fatal(err)
}
h.Peerstore().AddAddrs(
api.Host().ID(),
api.Host().Addrs(),
peerstore.PermanentAddrTTL,
)
return h
}
// MakeGet performs a GET request against the API.
func MakeGet(t *testing.T, api API, url string, resp interface{}) {
h := MakeHost(t, api)
defer h.Close()
c := HTTPClient(t, h, IsHTTPS(url))
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Origin", ClientOrigin)
httpResp, err := c.Do(req)
ProcessResp(t, httpResp, err, resp)
CheckHeaders(t, api.Headers(), url, httpResp.Header)
}
// MakePost performs a POST request against the API with the given body.
func MakePost(t *testing.T, api API, url string, body []byte, resp interface{}) {
MakePostWithContentType(t, api, url, body, "application/json", resp)
}
// MakePostWithContentType performs a POST with the given body and content-type.
func MakePostWithContentType(t *testing.T, api API, url string, body []byte, contentType string, resp interface{}) {
h := MakeHost(t, api)
defer h.Close()
c := HTTPClient(t, h, IsHTTPS(url))
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
req.Header.Set("Content-Type", contentType)
req.Header.Set("Origin", ClientOrigin)
httpResp, err := c.Do(req)
ProcessResp(t, httpResp, err, resp)
CheckHeaders(t, api.Headers(), url, httpResp.Header)
}
// MakeDelete performs a DELETE request against the given API.
func MakeDelete(t *testing.T, api API, url string, resp interface{}) {
h := MakeHost(t, api)
defer h.Close()
c := HTTPClient(t, h, IsHTTPS(url))
req, _ := http.NewRequest(http.MethodDelete, url, bytes.NewReader([]byte{}))
req.Header.Set("Origin", ClientOrigin)
httpResp, err := c.Do(req)
ProcessResp(t, httpResp, err, resp)
CheckHeaders(t, api.Headers(), url, httpResp.Header)
}
// MakeOptions performs an OPTIONS request against the given api.
func MakeOptions(t *testing.T, api API, url string, reqHeaders http.Header) http.Header {
h := MakeHost(t, api)
defer h.Close()
c := HTTPClient(t, h, IsHTTPS(url))
req, _ := http.NewRequest(http.MethodOptions, url, nil)
req.Header = reqHeaders
httpResp, err := c.Do(req)
ProcessResp(t, httpResp, err, nil)
return httpResp.Header
}
// MakeStreamingPost performs a POST request and uses ProcessStreamingResp
func MakeStreamingPost(t *testing.T, api API, url string, body io.Reader, contentType string, resp interface{}) {
h := MakeHost(t, api)
defer h.Close()
c := HTTPClient(t, h, IsHTTPS(url))
req, _ := http.NewRequest(http.MethodPost, url, body)
req.Header.Set("Content-Type", contentType)
req.Header.Set("Origin", ClientOrigin)
httpResp, err := c.Do(req)
ProcessStreamingResp(t, httpResp, err, resp, false)
CheckHeaders(t, api.Headers(), url, httpResp.Header)
}
// MakeStreamingGet performs a GET request and uses ProcessStreamingResp
func MakeStreamingGet(t *testing.T, api API, url string, resp interface{}, trailerError bool) {
h := MakeHost(t, api)
defer h.Close()
c := HTTPClient(t, h, IsHTTPS(url))
req, _ := http.NewRequest(http.MethodGet, url, nil)
req.Header.Set("Origin", ClientOrigin)
httpResp, err := c.Do(req)
ProcessStreamingResp(t, httpResp, err, resp, trailerError)
CheckHeaders(t, api.Headers(), url, httpResp.Header)
}
// Func is a function that runs a test with a given URL.
type Func func(t *testing.T, url URLFunc)
// BothEndpoints runs a test.Func against the http and p2p endpoints.
func BothEndpoints(t *testing.T, test Func) {
t.Run("in-parallel", func(t *testing.T) {
t.Run("http", func(t *testing.T) {
t.Parallel()
test(t, HTTPURL)
})
t.Run("libp2p", func(t *testing.T) {
t.Parallel()
test(t, P2pURL)
})
})
}
// HTTPSEndPoint runs the given test.Func against an HTTPs endpoint.
func HTTPSEndPoint(t *testing.T, test Func) {
t.Run("in-parallel", func(t *testing.T) {
t.Run("https", func(t *testing.T) {
t.Parallel()
test(t, httpsURL)
})
})
}