197 lines
6.7 KiB
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)),
|
|
}
|
|
}
|