storj/linksharing/handler_test.go
JT Olio 946ec201e2
metainfo: move api keys to part of the request (#3069)
What: we move api keys out of the grpc connection-level metadata on the client side and into the request protobufs directly. the server side still supports both mechanisms for backwards compatibility.

Why: dRPC won't support connection-level metadata. the only thing we currently use connection-level metadata for is api keys. we need to move all information needed by a request into the request protobuf itself for drpc support. check out the .proto changes for the main details.

One fun side-fact: Did you know that protobuf fields 1-15 are special and only use one byte for both the field number and type? Additionally did you know we don't use field 15 anywhere yet? So the new request header will use field 15, and should use field 15 on all protobufs going forward.

Please describe the tests: all existing tests should pass

Please describe the performance impact: none
2019-09-19 10:19:29 -06:00

285 lines
6.8 KiB
Go

// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package linksharing
import (
"context"
"net/http"
"net/http/httptest"
"path"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
"storj.io/storj/internal/testcontext"
"storj.io/storj/internal/testplanet"
"storj.io/storj/lib/uplink"
"storj.io/storj/pkg/storj"
)
func TestNewHandler(t *testing.T) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
uplink := newUplink(ctx, t)
defer ctx.Check(uplink.Close)
testCases := []struct {
name string
config HandlerConfig
err string
}{
{
name: "missing uplink",
config: HandlerConfig{
URLBase: "http://localhost",
},
err: "uplink is required",
},
{
name: "URL base must be http or https",
config: HandlerConfig{
Uplink: uplink,
URLBase: "gopher://chunks",
},
err: "URL base must be http:// or https://",
},
{
name: "URL base must contain host",
config: HandlerConfig{
Uplink: uplink,
URLBase: "http://",
},
err: "URL base must contain host",
},
{
name: "URL base can have a port",
config: HandlerConfig{
Uplink: uplink,
URLBase: "http://host:99",
},
},
{
name: "URL base can have a path",
config: HandlerConfig{
Uplink: uplink,
URLBase: "http://host/gopher",
},
},
{
name: "URL base must not contain user info",
config: HandlerConfig{
Uplink: uplink,
URLBase: "http://joe@host",
},
err: "URL base must not contain user info",
},
{
name: "URL base must not contain query values",
config: HandlerConfig{
Uplink: uplink,
URLBase: "http://host/?gopher=chunks",
},
err: "URL base must not contain query values",
},
{
name: "URL base must not contain a fragment",
config: HandlerConfig{
Uplink: uplink,
URLBase: "http://host/#gopher-chunks",
},
err: "URL base must not contain a fragment",
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
handler, err := NewHandler(zaptest.NewLogger(t), testCase.config)
if testCase.err != "" {
require.EqualError(t, err, testCase.err)
return
}
require.NoError(t, err)
require.NotNil(t, handler)
})
}
}
func TestHandlerRequests(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 2,
StorageNodeCount: 1,
UplinkCount: 1,
}, testHandlerRequests)
}
func testHandlerRequests(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
err := planet.Uplinks[0].Upload(ctx, planet.Satellites[0], "testbucket", "test/foo", []byte("FOO"))
require.NoError(t, err)
apiKey, err := uplink.ParseAPIKey(planet.Uplinks[0].APIKey[planet.Satellites[0].ID()].Serialize())
require.NoError(t, err)
scope, err := (&uplink.Scope{
SatelliteAddr: planet.Satellites[0].Addr(),
APIKey: apiKey,
EncryptionAccess: uplink.NewEncryptionAccessWithDefaultKey(storj.Key{}),
}).Serialize()
require.NoError(t, err)
testCases := []struct {
name string
method string
path string
status int
header http.Header
body string
}{
{
name: "invalid method",
method: "PUT",
status: http.StatusMethodNotAllowed,
body: "method not allowed\n",
},
{
name: "GET missing scope",
method: "GET",
status: http.StatusBadRequest,
body: "invalid request: missing scope\n",
},
{
name: "GET malformed scope",
method: "GET",
path: path.Join("BADSCOPE", "testbucket", "test/foo"),
status: http.StatusBadRequest,
body: "invalid request: invalid scope format\n",
},
{
name: "GET missing bucket",
method: "GET",
path: scope,
status: http.StatusBadRequest,
body: "invalid request: missing bucket\n",
},
{
name: "GET bucket not found",
method: "GET",
path: path.Join(scope, "someotherbucket", "test/foo"),
status: http.StatusNotFound,
body: "bucket not found\n",
},
{
name: "GET missing bucket path",
method: "GET",
path: path.Join(scope, "testbucket"),
status: http.StatusBadRequest,
body: "invalid request: missing bucket path\n",
},
{
name: "GET object not found",
method: "GET",
path: path.Join(scope, "testbucket", "test/bar"),
status: http.StatusNotFound,
body: "object not found\n",
},
{
name: "GET success",
method: "GET",
path: path.Join(scope, "testbucket", "test/foo"),
status: http.StatusOK,
body: "FOO",
},
{
name: "HEAD missing scope",
method: "HEAD",
status: http.StatusBadRequest,
body: "invalid request: missing scope\n",
},
{
name: "HEAD malformed scope",
method: "HEAD",
path: path.Join("BADSCOPE", "testbucket", "test/foo"),
status: http.StatusBadRequest,
body: "invalid request: invalid scope format\n",
},
{
name: "HEAD missing bucket",
method: "HEAD",
path: scope,
status: http.StatusBadRequest,
body: "invalid request: missing bucket\n",
},
{
name: "HEAD bucket not found",
method: "HEAD",
path: path.Join(scope, "someotherbucket", "test/foo"),
status: http.StatusNotFound,
body: "bucket not found\n",
},
{
name: "HEAD missing bucket path",
method: "HEAD",
path: path.Join(scope, "testbucket"),
status: http.StatusBadRequest,
body: "invalid request: missing bucket path\n",
},
{
name: "HEAD object not found",
method: "HEAD",
path: path.Join(scope, "testbucket", "test/bar"),
status: http.StatusNotFound,
body: "object not found\n",
},
{
name: "HEAD success",
method: "HEAD",
path: path.Join(scope, "testbucket", "test/foo"),
status: http.StatusFound,
header: http.Header{
"Location": []string{"http://localhost/" + path.Join(scope, "testbucket", "test/foo")},
},
body: "",
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
uplink := newUplink(ctx, t)
defer ctx.Check(uplink.Close)
handler, err := NewHandler(zaptest.NewLogger(t), HandlerConfig{
Uplink: uplink,
URLBase: "http://localhost",
})
require.NoError(t, err)
url := "http://localhost/" + testCase.path
w := httptest.NewRecorder()
r, err := http.NewRequest(testCase.method, url, nil)
require.NoError(t, err)
handler.ServeHTTP(w, r)
assert.Equal(t, testCase.status, w.Code, "status code does not match")
for h, v := range testCase.header {
assert.Equal(t, v, w.Header()[h], "%q header does not match", h)
}
assert.Equal(t, testCase.body, w.Body.String(), "body does not match")
})
}
}
func newUplink(ctx context.Context, tb testing.TB) *uplink.Uplink {
cfg := new(uplink.Config)
cfg.Volatile.Log = zaptest.NewLogger(tb)
cfg.Volatile.TLS.SkipPeerCAWhitelist = true
up, err := uplink.NewUplink(ctx, cfg)
require.NoError(tb, err)
return up
}