S3-compatible services such as Alibaba Cloud OSS, Tencent COS and Huawei OBS reject path-style URLs with 'SecondLevelDomainForbidden' and only accept virtual-hosted style (https://bucket.endpoint/key). This adds a UseVirtualHostedStyle field and SetVirtualHostedStyle() setter. When enabled and a custom Endpoint is set, the first path segment is treated as the bucket and prepended to the host as a subdomain. AWS S3 and MinIO accept both styles, so this is a no-op for them. Includes tests for OSS-style endpoint, http/https scheme preservation and the empty-path fallback used by ListBuckets-style operations.
194 lines
5.2 KiB
Go
194 lines
5.2 KiB
Go
package simples3
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type tConfig struct {
|
|
AccessKey string
|
|
SecretKey string
|
|
Endpoint string
|
|
Region string
|
|
}
|
|
|
|
func TestCustomEndpoint(t *testing.T) {
|
|
s3 := New("us-east-1", "AccessKey", "SuperSecretKey")
|
|
|
|
// no protocol specified, should default to https
|
|
s3.SetEndpoint("example.com")
|
|
if s3.getURL("bucket1") != "https://example.com/bucket1" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
|
|
// explicit http protocol
|
|
s3.SetEndpoint("http://localhost:9000")
|
|
if s3.getURL("bucket2") != "http://localhost:9000/bucket2" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
|
|
// explicit http protocol
|
|
s3.SetEndpoint("https://example.com")
|
|
if s3.getURL("bucket3") != "https://example.com/bucket3" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
|
|
// try with trailing slash
|
|
s3.SetEndpoint("https://example.com/foobar/")
|
|
if s3.getURL("bucket4") != "https://example.com/foobar/bucket4" {
|
|
t.Errorf("S3.SetEndpoint() got = %v", s3.Endpoint)
|
|
}
|
|
}
|
|
|
|
func TestGetURL(t *testing.T) {
|
|
s3 := New("us-east-1", "AccessKey", "SuperSecretKey")
|
|
|
|
type args struct {
|
|
bucket string
|
|
params []string
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
want string
|
|
}{
|
|
{
|
|
name: "getURL: basic test",
|
|
args: args{
|
|
bucket: "xyz",
|
|
},
|
|
want: "https://s3.us-east-1.amazonaws.com/xyz",
|
|
},
|
|
{
|
|
name: "getURL: multiple parameters",
|
|
args: args{
|
|
bucket: "xyz",
|
|
params: []string{"hello", "world"},
|
|
},
|
|
want: "https://s3.us-east-1.amazonaws.com/xyz/hello/world",
|
|
},
|
|
{
|
|
name: "getURL: special characters",
|
|
args: args{
|
|
bucket: "xyz",
|
|
params: []string{"hello, world!", "#!@$%^&*(1).txt"},
|
|
},
|
|
want: "https://s3.us-east-1.amazonaws.com/xyz/hello%2C%20world%21/%23%21%40%24%25%5E%26%2A%281%29.txt",
|
|
},
|
|
}
|
|
|
|
for _, testcase := range tests {
|
|
tt := testcase
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
url := s3.getURL(tt.args.bucket, tt.args.params...)
|
|
if url != tt.want {
|
|
t.Errorf("S3.getURL() got = %v, want %v", url, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVirtualHostedStyle(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
endpoint string
|
|
path string
|
|
args []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "OSS-style: bucket + key with custom endpoint, no scheme",
|
|
endpoint: "oss-cn-hangzhou.aliyuncs.com",
|
|
path: "mybucket",
|
|
args: []string{"path/to/key.txt"},
|
|
want: "https://mybucket.oss-cn-hangzhou.aliyuncs.com/path/to/key.txt",
|
|
},
|
|
{
|
|
name: "OSS-style: bucket only (e.g. ListObjects)",
|
|
endpoint: "oss-cn-hangzhou.aliyuncs.com",
|
|
path: "mybucket",
|
|
want: "https://mybucket.oss-cn-hangzhou.aliyuncs.com",
|
|
},
|
|
{
|
|
name: "explicit https scheme preserved",
|
|
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
|
|
path: "mybucket",
|
|
want: "https://mybucket.oss-cn-hangzhou.aliyuncs.com",
|
|
},
|
|
{
|
|
name: "explicit http scheme preserved (for local MinIO)",
|
|
endpoint: "http://localhost:9000",
|
|
path: "mybucket",
|
|
want: "http://mybucket.localhost:9000",
|
|
},
|
|
{
|
|
name: "trailing path on endpoint stripped before subdomain",
|
|
endpoint: "https://oss-cn-hangzhou.aliyuncs.com/extra/",
|
|
path: "mybucket",
|
|
want: "https://mybucket.oss-cn-hangzhou.aliyuncs.com",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s3 := New("cn-hangzhou", "AccessKey", "SecretKey").
|
|
SetEndpoint(tt.endpoint).
|
|
SetVirtualHostedStyle(true)
|
|
got := s3.getURL(tt.path, tt.args...)
|
|
if got != tt.want {
|
|
t.Errorf("virtual-hosted getURL() got = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVirtualHostedStyleDefaultOff(t *testing.T) {
|
|
// Without SetVirtualHostedStyle(true) we must keep the original
|
|
// path-style behaviour, so the S3-SDK upgrade stays non-breaking.
|
|
s3 := New("us-east-1", "AccessKey", "SecretKey").
|
|
SetEndpoint("https://oss-cn-hangzhou.aliyuncs.com")
|
|
if got := s3.getURL("mybucket", "k.txt"); got != "https://oss-cn-hangzhou.aliyuncs.com/mybucket/k.txt" {
|
|
t.Errorf("default path-style broken: got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestVirtualHostedStyleEmptyPath(t *testing.T) {
|
|
// When path is empty, virtual-hosted style can't be applied
|
|
// (no bucket to use as subdomain). Caller still gets the original
|
|
// path-style URL so ListBuckets-style operations still work.
|
|
s3 := New("us-east-1", "AccessKey", "SecretKey").
|
|
SetEndpoint("https://oss-cn-hangzhou.aliyuncs.com").
|
|
SetVirtualHostedStyle(true)
|
|
if got := s3.getURL(""); got != "https://oss-cn-hangzhou.aliyuncs.com/" {
|
|
t.Errorf("empty path should fall back to path-style: got %q", got)
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
func uploadTestFiles(t *testing.T, s3 *S3, bucket string, filenames []string) {
|
|
for _, filename := range filenames {
|
|
content := strings.NewReader("test content for " + filename)
|
|
_, err := s3.FilePut(UploadInput{
|
|
Bucket: bucket,
|
|
ObjectKey: filename,
|
|
ContentType: "text/plain",
|
|
Body: content,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Failed to upload test file %s: %v", filename, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func cleanupTestFiles(t *testing.T, s3 *S3, bucket string, filenames []string) {
|
|
for _, filename := range filenames {
|
|
err := s3.FileDelete(DeleteInput{
|
|
Bucket: bucket,
|
|
ObjectKey: filename,
|
|
})
|
|
if err != nil {
|
|
t.Logf("Failed to cleanup test file %s: %v", filename, err)
|
|
}
|
|
}
|
|
}
|