object_attributes_test.go
Rama main · réplica de lectura del repositorio original.
package predastore_test
import (
"bytes"
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/hex"
"io"
"net/http"
neturl "net/url"
"strconv"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/mulgadc/bluebottle/pkg/masterkey"
"github.com/mulgadc/predastore"
"github.com/mulgadc/predastore/internal/config"
"github.com/mulgadc/predastore/internal/testcerts"
"github.com/stretchr/testify/require"
)
const (
attrsAccessKey = "AKIAIOSFODNN7EXAMPLE"
attrsSecretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
attrsRegion = "ap-southeast-2"
attrsBucket = "attributes"
)
// startAuthedHost runs one process with a gate, a meta replica and three blob
// nodes, one service account and one bucket it owns, and returns the gate's
// URL and a client that trusts its certificate.
func startAuthedHost(t *testing.T) (string, *http.Client) {
t.Helper()
certPath, keyPath, pool := testcerts.Generate(t)
secret := make([]byte, 32)
_, err := rand.Read(secret)
require.NoError(t, err)
key, err := masterkey.New(secret)
require.NoError(t, err)
gatePort, adminPort := freePort(t), freePort(t)
nodes := []config.Node{
{ID: 1, Role: config.RoleGate, Port: gatePort, BindAddr: "127.0.0.1"},
{ID: 2, Role: config.RoleMeta, Port: 7001},
}
for i := range 3 {
nodes = append(nodes, config.Node{ID: config.NodeID(3 + i), Role: config.RoleBlob, Port: 7100 + i})
}
const account = "123456789012"
cfg := &config.Config{
Version: config.Version,
Region: attrsRegion,
RS: config.RS{Data: 2, Parity: 1},
Hosts: []config.Host{{
ID: 1, Addr: "127.0.0.1", DataDir: t.TempDir(),
TLSCert: certPath, TLSKey: keyPath, AdminPort: adminPort, Nodes: nodes,
}},
Buckets: []config.Bucket{{Name: attrsBucket, Region: attrsRegion, AccountID: account}},
Auth: []config.AuthEntry{{
AccessKeyID: attrsAccessKey, SecretAccessKey: attrsSecretKey, AccountID: account,
Policy: []config.PolicyRule{{Bucket: "*", Actions: []string{"s3:*"}}},
}},
}
require.NoError(t, cfg.Validate())
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() {
done <- predastore.Run(ctx, predastore.Options{Config: cfg, HostID: 1, MasterKey: key})
}()
t.Cleanup(func() {
cancel()
select {
case <-done:
case <-time.After(15 * time.Second):
t.Error("host did not stop")
}
})
awaitProbe(t, "http://127.0.0.1:"+strconv.Itoa(adminPort)+"/readyz", http.StatusOK)
client := &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool}}}
return "https://127.0.0.1:" + strconv.Itoa(gatePort), client
}
// signedRequest builds a SigV4-signed request, which is what every S3 client
// sends and what the gate verifies before any handler runs.
func signedRequest(t *testing.T, method, url string, body []byte, headers map[string]string) *http.Request {
t.Helper()
req, err := http.NewRequest(method, url, bytes.NewReader(body)) //nolint:noctx // bounded by the test.
require.NoError(t, err)
req.ContentLength = int64(len(body))
for k, v := range headers {
req.Header.Set(k, v)
}
sum := sha256.Sum256(body)
payloadHash := hex.EncodeToString(sum[:])
req.Header.Set("X-Amz-Content-Sha256", payloadHash)
signer := v4.NewSigner(func(o *v4.SignerOptions) { o.DisableURIPathEscaping = true })
require.NoError(t, signer.SignHTTP(context.Background(),
aws.Credentials{AccessKeyID: attrsAccessKey, SecretAccessKey: attrsSecretKey},
req, payloadHash, "s3", attrsRegion, time.Now().UTC()))
return req
}
// A signed PUT's Content-Type and x-amz-meta-* headers come back on HEAD and
// GET through the whole stack: listener, SigV4, meta replication and shards.
func TestObjectAttributesSurviveTheWholeStack(t *testing.T) {
base, client := startAuthedHost(t)
url := base + "/" + attrsBucket + "/doc.txt"
// The gate binds after the election /readyz waits on, so the first request
// can still arrive before its listener exists.
var put *http.Response
deadline := time.Now().Add(20 * time.Second)
for {
var err error
put, err = client.Do(signedRequest(t, http.MethodPut, url, []byte("bar"), map[string]string{
"Content-Type": "audio/ogg", "X-Amz-Meta-Key1": "value1", "X-Amz-Meta-Empty": "",
"Cache-Control": "public, max-age=31536000, immutable", "Content-Disposition": `attachment; filename="doc.txt"`,
}))
if err == nil || time.Now().After(deadline) {
require.NoError(t, err)
break
}
time.Sleep(50 * time.Millisecond)
}
body, _ := io.ReadAll(put.Body)
_ = put.Body.Close()
require.Equal(t, http.StatusOK, put.StatusCode, string(body))
for _, method := range []string{http.MethodHead, http.MethodGet} {
resp, err := client.Do(signedRequest(t, method, url, nil, nil))
require.NoError(t, err)
got, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, "%s: %s", method, got)
require.Equal(t, "audio/ogg", resp.Header.Get("Content-Type"), method)
require.Equal(t, "value1", resp.Header.Get("X-Amz-Meta-Key1"), method)
require.Equal(t, []string{""}, resp.Header.Values("X-Amz-Meta-Empty"), method)
require.Equal(t, "public, max-age=31536000, immutable", resp.Header.Get("Cache-Control"), method)
require.Equal(t, `attachment; filename="doc.txt"`, resp.Header.Get("Content-Disposition"), method)
if method == http.MethodGet {
require.Equal(t, "bar", string(got))
}
}
// A presigned URL carries its overrides in the signed query, which is how a
// browser download is given its file name.
req, err := http.NewRequest(http.MethodGet, //nolint:noctx // bounded by the test.
url+"?X-Amz-Expires=300&response-content-disposition="+neturl.QueryEscape(`attachment; filename="renamed.txt"`), nil)
require.NoError(t, err)
signer := v4.NewSigner(func(o *v4.SignerOptions) { o.DisableURIPathEscaping = true })
presigned, _, err := signer.PresignHTTP(context.Background(),
aws.Credentials{AccessKeyID: attrsAccessKey, SecretAccessKey: attrsSecretKey},
req, "UNSIGNED-PAYLOAD", "s3", attrsRegion, time.Now().UTC())
require.NoError(t, err)
resp, err := client.Get(presigned) //nolint:noctx // bounded by the test.
require.NoError(t, err)
got, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
require.Equal(t, http.StatusOK, resp.StatusCode, string(got))
require.Equal(t, `attachment; filename="renamed.txt"`, resp.Header.Get("Content-Disposition"))
require.Equal(t, "public, max-age=31536000, immutable", resp.Header.Get("Cache-Control"))
}