package assets import ( "bytes" "context" "errors" "fmt" "io" "net/http" "net/url" "strings" "syscall" "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 TestOSSHTTPClientPresignsPrivateGetURL(t *testing.T) { client := newTestOSSHTTPClient(t, func(*http.Request) *http.Response { t.Fatal("presigning must not perform a network request") return nil }) signed, err := client.SignGetURL(OSSObjectRequest{ Endpoint: "https://oss-cn-test.aliyuncs.com", Bucket: "bucket-a", Key: "photos/cat.png", }, time.Hour) if err != nil { t.Fatal(err) } parsed, err := url.Parse(signed) if err != nil { t.Fatal(err) } if parsed.Scheme != "https" || parsed.Host != "bucket-a.oss-cn-test.aliyuncs.com" || parsed.EscapedPath() != "/photos/cat.png" { t.Fatalf("signed URL target = %s", parsed) } query := parsed.Query() if query.Get("OSSAccessKeyId") != "access-key" || query.Get("Expires") != "1786582800" || query.Get("Signature") != "uA2xP4TwgB6YB17MaALkharfEmM=" { t.Fatalf("signed URL query = %#v", query) } if _, err := client.SignGetURL(OSSObjectRequest{Endpoint: "https://oss-cn-test.aliyuncs.com", Bucket: "bucket-a", Key: "photos/cat.png"}, 10*time.Hour); err == nil { t.Fatal("expected excessive signed URL lifetime to fail") } } 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 TestOSSTransportDiagnosticIsClassifiedAndDoesNotExposeRequestData(t *testing.T) { request, err := http.NewRequest(http.MethodPut, "https://private-bucket.oss-cn-guangzhou-internal.aliyuncs.com/uploads/private-file.png?credential=secret", nil) if err != nil { t.Fatal(err) } request.Header.Set("Authorization", "OSS access-key:private-signature") diagnostic := diagnoseOSSTransport(request, &url.Error{ Op: "Put", URL: request.URL.String(), Err: syscall.ECONNRESET, }) if diagnostic.Method != http.MethodPut || diagnostic.EndpointClass != "internal" || diagnostic.ErrorClass != "connection_reset" { t.Fatalf("diagnostic = %#v", diagnostic) } serialized := fmt.Sprintf("%+v", diagnostic) for _, secret := range []string{"private-bucket", "private-file", "credential", "access-key", "private-signature"} { if strings.Contains(serialized, secret) { t.Fatalf("diagnostic leaks %q: %s", secret, serialized) } } } 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)), } }