Files
tamabox/internal/storage/s3_client_test.go
T

127 lines
5.1 KiB
Go

package storage
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNormalizeS3Endpoint(t *testing.T) {
tests := []struct {
name string
endpoint string
bucket string
want string
wantErr bool
}{
{name: "empty path", endpoint: "https://account.example.com", bucket: "images", want: "https://account.example.com"},
{name: "strip exact bucket path", endpoint: "https://account.example.com/images", bucket: "images", want: "https://account.example.com"},
{name: "httptest HTTP supported", endpoint: "http://127.0.0.1:12345/images", bucket: "images", want: "http://127.0.0.1:12345"},
{name: "root path rejected", endpoint: "https://account.example.com/", bucket: "images", wantErr: true},
{name: "different path rejected", endpoint: "https://account.example.com/other", bucket: "images", wantErr: true},
{name: "bucket trailing slash rejected", endpoint: "https://account.example.com/images/", bucket: "images", wantErr: true},
{name: "encoded bucket rejected", endpoint: "https://account.example.com/%69mages", bucket: "images", wantErr: true},
{name: "relative rejected", endpoint: "account.example.com", bucket: "images", wantErr: true},
{name: "empty hostname rejected", endpoint: "https://:443", bucket: "images", wantErr: true},
{name: "unsupported scheme rejected", endpoint: "ftp://account.example.com", bucket: "images", wantErr: true},
{name: "query rejected", endpoint: "https://account.example.com?x=1", bucket: "images", wantErr: true},
{name: "empty query rejected", endpoint: "https://account.example.com?", bucket: "images", wantErr: true},
{name: "fragment rejected", endpoint: "https://account.example.com#fragment", bucket: "images", wantErr: true},
{name: "userinfo rejected", endpoint: "https://[email protected]", bucket: "images", wantErr: true},
{name: "empty bucket rejected", endpoint: "https://account.example.com", bucket: "", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := normalizeS3Endpoint(tt.endpoint, tt.bucket)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
func TestNewS3ClientConfiguration(t *testing.T) {
client, err := NewS3Client(context.Background(), "https://account.example.com/images", "images", "test-id", "test-secret")
require.NoError(t, err)
options := client.Options()
assert.Equal(t, "auto", options.Region)
assert.True(t, options.UsePathStyle)
credentials, err := options.Credentials.Retrieve(context.Background())
require.NoError(t, err)
assert.Equal(t, "test-id", credentials.AccessKeyID)
assert.Equal(t, "test-secret", credentials.SecretAccessKey)
httpClient, ok := options.HTTPClient.(*http.Client)
require.True(t, ok)
assert.Zero(t, httpClient.Timeout, "全局 Client.Timeout 会截断大对象传输")
transport, ok := httpClient.Transport.(*http.Transport)
require.True(t, ok)
assert.NotNil(t, transport.DialContext)
assert.Positive(t, transport.TLSHandshakeTimeout)
assert.Positive(t, transport.ResponseHeaderTimeout)
assert.Positive(t, transport.ExpectContinueTimeout)
}
func TestS3PutObjectRequestShape(t *testing.T) {
type capturedRequest struct {
method string
path string
body string
contentType string
cacheControl string
authorization string
}
captured := make(chan capturedRequest, 1)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
captured <- capturedRequest{
method: r.Method,
path: r.URL.Path,
body: string(body),
contentType: r.Header.Get("Content-Type"),
cacheControl: r.Header.Get("Cache-Control"),
authorization: r.Header.Get("Authorization"),
}
w.Header().Set("ETag", `"test-etag"`)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
client, err := NewS3Client(context.Background(), server.URL+"/images", "images", "test-id", "test-secret")
require.NoError(t, err)
err = PutObject(context.Background(), client, &s3.PutObjectInput{
Bucket: aws.String("images"),
Key: aws.String("2026/11/picture.webp"),
Body: bytes.NewReader([]byte("fake-image")),
ContentLength: aws.Int64(10),
ContentType: aws.String("image/webp"),
CacheControl: aws.String("public, max-age=31536000, immutable"),
})
require.NoError(t, err)
request := <-captured
assert.Equal(t, http.MethodPut, request.method)
assert.Equal(t, "/images/2026/11/picture.webp", request.path, "bucket 必须且只能出现一次")
assert.Equal(t, "fake-image", request.body)
assert.Equal(t, "image/webp", request.contentType)
assert.Equal(t, "public, max-age=31536000, immutable", request.cacheControl)
assert.Contains(t, request.authorization, "Credential=test-id/")
assert.Contains(t, request.authorization, "/auto/s3/aws4_request")
}