Ctrl K
ComunidadGuías…

object_attributes_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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"))
}