depot/packages/networking/ipfs-cluster/api/common/api_test.go

644 lines
15 KiB
Go

package common
import (
"context"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/httputil"
"os"
"path/filepath"
"testing"
"time"
"github.com/ipfs-cluster/ipfs-cluster/api"
"github.com/ipfs-cluster/ipfs-cluster/api/common/test"
rpctest "github.com/ipfs-cluster/ipfs-cluster/test"
libp2p "github.com/libp2p/go-libp2p"
rpc "github.com/libp2p/go-libp2p-gorpc"
ma "github.com/multiformats/go-multiaddr"
)
const (
SSLCertFile = "test/server.crt"
SSLKeyFile = "test/server.key"
validUserName = "validUserName"
validUserPassword = "validUserPassword"
adminUserName = "adminUserName"
adminUserPassword = "adminUserPassword"
invalidUserName = "invalidUserName"
invalidUserPassword = "invalidUserPassword"
)
var (
validToken, _ = generateSignedTokenString(validUserName, validUserPassword)
invalidToken, _ = generateSignedTokenString(invalidUserName, invalidUserPassword)
)
func routes(c *rpc.Client) []Route {
return []Route{
{
"Test",
"GET",
"/test",
func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.Write([]byte(`{ "thisis": "atest" }`))
},
},
}
}
func testAPIwithConfig(t *testing.T, cfg *Config, name string) *API {
ctx := context.Background()
apiMAddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/tcp/0")
h, err := libp2p.New(libp2p.ListenAddrs(apiMAddr))
if err != nil {
t.Fatal(err)
}
cfg.HTTPListenAddr = []ma.Multiaddr{apiMAddr}
rest, err := NewAPIWithHost(ctx, cfg, h, routes)
if err != nil {
t.Fatalf("should be able to create a new %s API: %s", name, err)
}
// No keep alive for tests
rest.server.SetKeepAlivesEnabled(false)
rest.SetClient(rpctest.NewMockRPCClient(t))
return rest
}
func testAPI(t *testing.T) *API {
cfg := newDefaultTestConfig(t)
cfg.CORSAllowedOrigins = []string{test.ClientOrigin}
cfg.CORSAllowedMethods = []string{"GET", "POST", "DELETE"}
//cfg.CORSAllowedHeaders = []string{"Content-Type"}
cfg.CORSMaxAge = 10 * time.Minute
return testAPIwithConfig(t, cfg, "basic")
}
func testHTTPSAPI(t *testing.T) *API {
cfg := newDefaultTestConfig(t)
cfg.PathSSLCertFile = SSLCertFile
cfg.PathSSLKeyFile = SSLKeyFile
var err error
cfg.TLS, err = newTLSConfig(cfg.PathSSLCertFile, cfg.PathSSLKeyFile)
if err != nil {
t.Fatal(err)
}
return testAPIwithConfig(t, cfg, "https")
}
func testAPIwithBasicAuth(t *testing.T) *API {
cfg := newDefaultTestConfig(t)
cfg.BasicAuthCredentials = map[string]string{
validUserName: validUserPassword,
adminUserName: adminUserPassword,
}
return testAPIwithConfig(t, cfg, "Basic Authentication")
}
func TestAPIShutdown(t *testing.T) {
ctx := context.Background()
rest := testAPI(t)
err := rest.Shutdown(ctx)
if err != nil {
t.Error("should shutdown cleanly: ", err)
}
// test shutting down twice
rest.Shutdown(ctx)
}
func TestHTTPSTestEndpoint(t *testing.T) {
ctx := context.Background()
rest := testAPI(t)
httpsrest := testHTTPSAPI(t)
defer rest.Shutdown(ctx)
defer httpsrest.Shutdown(ctx)
tf := func(t *testing.T, url test.URLFunc) {
r := make(map[string]string)
test.MakeGet(t, rest, url(rest)+"/test", &r)
if r["thisis"] != "atest" {
t.Error("expected correct body")
}
}
httpstf := func(t *testing.T, url test.URLFunc) {
r := make(map[string]string)
test.MakeGet(t, httpsrest, url(httpsrest)+"/test", &r)
if r["thisis"] != "atest" {
t.Error("expected correct body")
}
}
test.BothEndpoints(t, tf)
test.HTTPSEndPoint(t, httpstf)
}
func TestAPILogging(t *testing.T) {
ctx := context.Background()
cfg := newDefaultTestConfig(t)
logFile, err := filepath.Abs("http.log")
if err != nil {
t.Fatal(err)
}
cfg.HTTPLogFile = logFile
rest := testAPIwithConfig(t, cfg, "log_enabled")
defer os.Remove(cfg.HTTPLogFile)
info, err := os.Stat(cfg.HTTPLogFile)
if err != nil {
t.Fatal(err)
}
if info.Size() > 0 {
t.Errorf("expected empty log file")
}
id := api.ID{}
test.MakeGet(t, rest, test.HTTPURL(rest)+"/test", &id)
info, err = os.Stat(cfg.HTTPLogFile)
if err != nil {
t.Fatal(err)
}
size1 := info.Size()
if size1 == 0 {
t.Error("did not expect an empty log file")
}
// Restart API and make sure that logs are being appended
rest.Shutdown(ctx)
rest = testAPIwithConfig(t, cfg, "log_enabled")
defer rest.Shutdown(ctx)
test.MakeGet(t, rest, test.HTTPURL(rest)+"/id", &id)
info, err = os.Stat(cfg.HTTPLogFile)
if err != nil {
t.Fatal(err)
}
size2 := info.Size()
if size2 == 0 {
t.Error("did not expect an empty log file")
}
if !(size2 > size1) {
t.Error("logs were not appended")
}
}
func TestNotFoundHandler(t *testing.T) {
ctx := context.Background()
rest := testAPI(t)
defer rest.Shutdown(ctx)
tf := func(t *testing.T, url test.URLFunc) {
bytes := make([]byte, 10)
for i := 0; i < 10; i++ {
bytes[i] = byte(65 + rand.Intn(25)) //A=65 and Z = 65+25
}
var errResp api.Error
test.MakePost(t, rest, url(rest)+"/"+string(bytes), []byte{}, &errResp)
if errResp.Code != 404 {
t.Errorf("expected error not found: %+v", errResp)
}
var errResp1 api.Error
test.MakeGet(t, rest, url(rest)+"/"+string(bytes), &errResp1)
if errResp1.Code != 404 {
t.Errorf("expected error not found: %+v", errResp)
}
}
test.BothEndpoints(t, tf)
}
func TestCORS(t *testing.T) {
ctx := context.Background()
rest := testAPI(t)
defer rest.Shutdown(ctx)
type testcase struct {
method string
path string
}
tf := func(t *testing.T, url test.URLFunc) {
reqHeaders := make(http.Header)
reqHeaders.Set("Origin", "myorigin")
reqHeaders.Set("Access-Control-Request-Headers", "Content-Type")
for _, tc := range []testcase{
{"GET", "/test"},
// testcase{},
} {
reqHeaders.Set("Access-Control-Request-Method", tc.method)
headers := test.MakeOptions(t, rest, url(rest)+tc.path, reqHeaders)
aorigin := headers.Get("Access-Control-Allow-Origin")
amethods := headers.Get("Access-Control-Allow-Methods")
aheaders := headers.Get("Access-Control-Allow-Headers")
acreds := headers.Get("Access-Control-Allow-Credentials")
maxage := headers.Get("Access-Control-Max-Age")
if aorigin != "myorigin" {
t.Error("Bad ACA-Origin:", aorigin)
}
if amethods != tc.method {
t.Error("Bad ACA-Methods:", amethods)
}
if aheaders != "Content-Type" {
t.Error("Bad ACA-Headers:", aheaders)
}
if acreds != "true" {
t.Error("Bad ACA-Credentials:", acreds)
}
if maxage != "600" {
t.Error("Bad AC-Max-Age:", maxage)
}
}
}
test.BothEndpoints(t, tf)
}
type responseChecker func(*http.Response) error
type requestShaper func(*http.Request) error
type httpTestcase struct {
method string
path string
header http.Header
body io.ReadCloser
shaper requestShaper
checker responseChecker
}
func httpStatusCodeChecker(resp *http.Response, expectedStatus int) error {
if resp.StatusCode == expectedStatus {
return nil
}
return fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
}
func assertHTTPStatusIsUnauthoriazed(resp *http.Response) error {
return httpStatusCodeChecker(resp, http.StatusUnauthorized)
}
func assertHTTPStatusIsTooLarge(resp *http.Response) error {
return httpStatusCodeChecker(resp, http.StatusRequestHeaderFieldsTooLarge)
}
func makeHTTPStatusNegatedAssert(checker responseChecker) responseChecker {
return func(resp *http.Response) error {
if checker(resp) == nil {
return fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode)
}
return nil
}
}
func (tc *httpTestcase) getTestFunction(api *API) test.Func {
return func(t *testing.T, prefixMaker test.URLFunc) {
h := test.MakeHost(t, api)
defer h.Close()
url := prefixMaker(api) + tc.path
c := test.HTTPClient(t, h, test.IsHTTPS(url))
req, err := http.NewRequest(tc.method, url, tc.body)
if err != nil {
t.Fatal("Failed to assemble a HTTP request: ", err)
}
if tc.header != nil {
req.Header = tc.header
}
if tc.shaper != nil {
err := tc.shaper(req)
if err != nil {
t.Fatal("Failed to shape a HTTP request: ", err)
}
}
resp, err := c.Do(req)
if err != nil {
t.Fatal("Failed to make a HTTP request: ", err)
}
if tc.checker != nil {
if err := tc.checker(resp); err != nil {
r, e := httputil.DumpRequest(req, true)
if e != nil {
t.Errorf("Assertion failed with: %q", err)
} else {
t.Errorf("Assertion failed with: %q on request: \n%.100s", err, r)
}
}
}
}
}
func makeBasicAuthRequestShaper(username, password string) requestShaper {
return func(req *http.Request) error {
req.SetBasicAuth(username, password)
return nil
}
}
func makeTokenAuthRequestShaper(token string) requestShaper {
return func(req *http.Request) error {
req.Header.Set("Authorization", "Bearer "+token)
return nil
}
}
func makeLongHeaderShaper(size int) requestShaper {
return func(req *http.Request) error {
for sz := size; sz > 0; sz -= 8 {
req.Header.Add("Foo", "bar")
}
return nil
}
}
func TestBasicAuth(t *testing.T) {
ctx := context.Background()
rest := testAPIwithBasicAuth(t)
defer rest.Shutdown(ctx)
for _, tc := range []httpTestcase{
{},
{
method: "",
path: "",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "POST",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "DELETE",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "HEAD",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "OPTIONS", // Always allowed for CORS
path: "/foo",
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "PUT",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "TRACE",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "CONNECT",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "BAR",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeBasicAuthRequestShaper(invalidUserName, invalidUserPassword),
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeBasicAuthRequestShaper(validUserName, invalidUserPassword),
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeBasicAuthRequestShaper(invalidUserName, validUserPassword),
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeBasicAuthRequestShaper(adminUserName, validUserPassword),
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeBasicAuthRequestShaper(validUserName, validUserPassword),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "POST",
path: "/foo",
shaper: makeBasicAuthRequestShaper(validUserName, validUserPassword),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "DELETE",
path: "/foo",
shaper: makeBasicAuthRequestShaper(validUserName, validUserPassword),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "BAR",
path: "/foo",
shaper: makeBasicAuthRequestShaper(validUserName, validUserPassword),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "GET",
path: "/test",
shaper: makeBasicAuthRequestShaper(validUserName, validUserPassword),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
} {
test.BothEndpoints(t, tc.getTestFunction(rest))
}
}
func TestTokenAuth(t *testing.T) {
ctx := context.Background()
rest := testAPIwithBasicAuth(t)
defer rest.Shutdown(ctx)
for _, tc := range []httpTestcase{
{},
{
method: "",
path: "",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "POST",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "DELETE",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "HEAD",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "OPTIONS", // Always allowed for CORS
path: "/foo",
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "PUT",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "TRACE",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "CONNECT",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "BAR",
path: "/foo",
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeTokenAuthRequestShaper(invalidToken),
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeTokenAuthRequestShaper(invalidToken),
checker: assertHTTPStatusIsUnauthoriazed,
},
{
method: "GET",
path: "/foo",
shaper: makeTokenAuthRequestShaper(validToken),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "POST",
path: "/foo",
shaper: makeTokenAuthRequestShaper(validToken),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "DELETE",
path: "/foo",
shaper: makeTokenAuthRequestShaper(validToken),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "BAR",
path: "/foo",
shaper: makeTokenAuthRequestShaper(validToken),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
{
method: "GET",
path: "/test",
shaper: makeTokenAuthRequestShaper(validToken),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsUnauthoriazed),
},
} {
test.BothEndpoints(t, tc.getTestFunction(rest))
}
}
func TestLimitMaxHeaderSize(t *testing.T) {
maxHeaderBytes := 4 * DefaultMaxHeaderBytes
cfg := newTestConfig()
cfg.MaxHeaderBytes = maxHeaderBytes
ctx := context.Background()
rest := testAPIwithConfig(t, cfg, "http with maxHeaderBytes")
defer rest.Shutdown(ctx)
for _, tc := range []httpTestcase{
{
method: "GET",
path: "/foo",
shaper: makeLongHeaderShaper(maxHeaderBytes * 2),
checker: assertHTTPStatusIsTooLarge,
},
{
method: "GET",
path: "/foo",
shaper: makeLongHeaderShaper(maxHeaderBytes / 2),
checker: makeHTTPStatusNegatedAssert(assertHTTPStatusIsTooLarge),
},
} {
test.BothEndpoints(t, tc.getTestFunction(rest))
}
}