Files
NianAIGC/backend/internal/assets/oss_http_test.go

197 lines
6.7 KiB
Go

package assets
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
"time"
)
type ossRoundTripFunc func(*http.Request) (*http.Response, error)
func (f ossRoundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestOSSHTTPClientSignsPutAndBuildsVirtualHostURL(t *testing.T) {
client := newTestOSSHTTPClient(t, func(r *http.Request) *http.Response {
if r.Method != http.MethodPut || r.URL.String() != "https://bucket-a.oss-cn-test.aliyuncs.com/photos/cat.png" {
t.Fatalf("request = %s %s", r.Method, r.URL)
}
if r.Host != "bucket-a.oss-cn-test.aliyuncs.com" || r.Header.Get("Date") != "Thu, 13 Aug 2026 00:00:00 GMT" {
t.Fatalf("host/date = %q / %q", r.Host, r.Header.Get("Date"))
}
if r.Header.Get("Content-Type") != "image/png" || r.Header.Get("Content-Length") != "3" || r.ContentLength != 3 {
t.Fatalf("content type/length = %q / %q / %d", r.Header.Get("Content-Type"), r.Header.Get("Content-Length"), r.ContentLength)
}
if got := r.Header.Get("Authorization"); got != "OSS access-key:ZBQxu+qcAZzglIi4GbBMhAIZIYs=" {
t.Fatalf("Authorization = %q", got)
}
body, _ := io.ReadAll(r.Body)
if string(body) != "png" {
t.Fatalf("body = %q", body)
}
return ossHTTPResponse(http.StatusOK, "", nil)
})
err := client.Put(context.Background(), OSSPutRequest{
Endpoint: "oss-cn-test.aliyuncs.com/", Bucket: "bucket-a", Key: "photos/cat.png",
ContentType: "image/png", Body: strings.NewReader("png"), Size: 3,
})
if err != nil {
t.Fatal(err)
}
}
func TestOSSHTTPClientSignsACLGetDeleteAndStreamsGetMetadata(t *testing.T) {
var calls int
client := newTestOSSHTTPClient(t, func(r *http.Request) *http.Response {
calls++
if r.URL.Host != "bucket-a.oss-cn-test.aliyuncs.com" || r.URL.Path != "/photos/cat.png" {
t.Fatalf("URL = %s", r.URL)
}
switch calls {
case 1:
if r.Method != http.MethodPut || r.URL.RawQuery != "acl" || r.Header.Get("x-oss-object-acl") != OSSACLPublicRead {
t.Fatalf("ACL request = %s %s headers=%v", r.Method, r.URL, r.Header)
}
if got := r.Header.Get("Authorization"); got != "OSS access-key:B8m2FqdnBSJTA4F6L3o7NZkkYTU=" {
t.Fatalf("ACL Authorization = %q", got)
}
return ossHTTPResponse(http.StatusOK, "", nil)
case 2:
if r.Method != http.MethodGet || r.Header.Get("Authorization") != "OSS access-key:+do4MvonQT0wF7/Pwl+7+Eqze9g=" {
t.Fatalf("GET request = %s auth=%q", r.Method, r.Header.Get("Authorization"))
}
return ossHTTPResponse(http.StatusOK, "image/png", []byte("streamed"))
case 3:
if r.Method != http.MethodDelete || r.Header.Get("Authorization") != "OSS access-key:ykyNN5k2axmVkPpEqzNsEVHCmGU=" {
t.Fatalf("DELETE request = %s auth=%q", r.Method, r.Header.Get("Authorization"))
}
return ossHTTPResponse(http.StatusNoContent, "", nil)
default:
t.Fatalf("unexpected call %d", calls)
return nil
}
})
ctx := context.Background()
object := OSSObjectRequest{Endpoint: "https://bucket-a.oss-cn-test.aliyuncs.com", Bucket: "bucket-a", Key: "photos/cat.png"}
if err := client.SetACL(ctx, OSSACLRequest{Endpoint: object.Endpoint, Bucket: object.Bucket, Key: object.Key, ACL: OSSACLPublicRead}); err != nil {
t.Fatal(err)
}
blob, err := client.Get(ctx, object)
if err != nil {
t.Fatal(err)
}
if blob.ContentType != "image/png" || blob.Size != 8 {
t.Fatalf("blob metadata = %#v", blob)
}
got, _ := io.ReadAll(blob.Body)
_ = blob.Body.Close()
if string(got) != "streamed" {
t.Fatalf("body = %q", got)
}
if err := client.Delete(ctx, object); err != nil {
t.Fatal(err)
}
}
func TestOSSHTTPClientUsesPathStyleForExplicitBucketPathAndEscapesKey(t *testing.T) {
client := newTestOSSHTTPClient(t, func(r *http.Request) *http.Response {
if got := r.URL.String(); got != "https://proxy.test/storage/bucket-a/a%20b/%E7%8C%AB.png" {
t.Fatalf("URL = %q", got)
}
return ossHTTPResponse(http.StatusNoContent, "", nil)
})
if err := client.Delete(context.Background(), OSSObjectRequest{
Endpoint: "https://proxy.test/storage/bucket-a", Bucket: "bucket-a", Key: "a b/猫.png",
}); err != nil {
t.Fatal(err)
}
}
func TestOSSHTTPClientMapsServiceErrorsWithoutLeakingResponseOrCredentials(t *testing.T) {
client := newTestOSSHTTPClient(t, func(*http.Request) *http.Response {
return &http.Response{
StatusCode: http.StatusNotFound,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader(
`<Error><Code>NoSuchKey</Code><Message>access-key secret-key private-object</Message></Error>`,
)),
}
})
_, err := client.Get(context.Background(), OSSObjectRequest{Endpoint: "https://oss-cn-test.aliyuncs.com", Bucket: "bucket-a", Key: "private-object"})
var ossErr *OSSError
if !errors.As(err, &ossErr) || ossErr.Status != http.StatusNotFound || ossErr.Code != "NoSuchKey" {
t.Fatalf("error = %#v", err)
}
for _, secret := range []string{"access-key", "secret-key", "private-object"} {
if strings.Contains(err.Error(), secret) {
t.Fatalf("error leaks %q: %v", secret, err)
}
}
}
func TestOSSHTTPClientPropagatesContextCancellation(t *testing.T) {
client := newTestOSSHTTPClient(t, func(r *http.Request) *http.Response {
<-r.Context().Done()
return nil
})
ctx, cancel := context.WithCancel(context.Background())
cancel()
err := client.Delete(ctx, OSSObjectRequest{Endpoint: "https://oss-cn-test.aliyuncs.com", Bucket: "bucket-a", Key: "object"})
if !errors.Is(err, context.Canceled) {
t.Fatalf("error = %v, want context cancellation", err)
}
}
func TestNewOSSHTTPClientRejectsMissingDependencies(t *testing.T) {
for _, test := range []struct {
id, secret string
client *http.Client
}{
{"", "secret", http.DefaultClient},
{"id", "", http.DefaultClient},
{"id", "secret", nil},
} {
if _, err := NewOSSHTTPClient(test.id, test.secret, test.client, time.Now); err == nil {
t.Fatalf("NewOSSHTTPClient(%q, %q, %v) succeeded", test.id, test.secret, test.client)
}
}
}
func newTestOSSHTTPClient(t *testing.T, roundTrip func(*http.Request) *http.Response) *OSSHTTPClient {
t.Helper()
httpClient := &http.Client{Transport: ossRoundTripFunc(func(r *http.Request) (*http.Response, error) {
response := roundTrip(r)
if response == nil {
return nil, r.Context().Err()
}
response.Request = r
return response, nil
})}
client, err := NewOSSHTTPClient("access-key", "secret-key", httpClient, func() time.Time {
return time.Date(2026, 8, 13, 0, 0, 0, 0, time.UTC)
})
if err != nil {
t.Fatal(err)
}
return client
}
func ossHTTPResponse(status int, contentType string, body []byte) *http.Response {
header := make(http.Header)
if contentType != "" {
header.Set("Content-Type", contentType)
}
return &http.Response{
StatusCode: status,
Header: header,
Body: io.NopCloser(bytes.NewReader(body)),
ContentLength: int64(len(body)),
}
}