可靠性: 完善 R2 备份恢复与对象生命周期
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user