296 lines
8.4 KiB
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)
|
|
})
|
|
})
|
|
}
|