127 lines
5.1 KiB
Go
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")
|
|
}
|