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( `NoSuchKeyaccess-key secret-key private-object`, )), } }) _, 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)), } }