mirror of
https://github.com/hanzoai/replicate.git
synced 2026-10-30 18:23:20 +00:00
A version written into a manifest is written into every manifest that wants it, which is how eslint came to exist in a hundred and four forms across this estate and how a package came to resolve twice at run time. The catalog moves the number out of the manifest: a manifest names the package, the workspace names the version. Four are held below their latest because the newer release cannot be used, and the reason is recorded rather than the version alone: @noble/hashes 2 renamed every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels 3 points its types condition at the bundle.
1057 lines
31 KiB
Go
1057 lines
31 KiB
Go
package replicate_test
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hanzoai/replicate"
|
|
"github.com/hanzoai/replicate/file"
|
|
"github.com/hanzoai/replicate/nats"
|
|
"github.com/hanzoai/replicate/s3"
|
|
"github.com/hanzoai/replicate/sftp"
|
|
"github.com/hanzoai/replicate/webdav"
|
|
)
|
|
|
|
func TestNewReplicaClientFromURL(t *testing.T) {
|
|
t.Run("S3", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://mybucket/path/to/db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client.Type() != "s3" {
|
|
t.Errorf("expected type 's3', got %q", client.Type())
|
|
}
|
|
s3Client, ok := client.(*s3.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *s3.ReplicaClient, got %T", client)
|
|
}
|
|
if s3Client.Bucket != "mybucket" {
|
|
t.Errorf("expected bucket 'mybucket', got %q", s3Client.Bucket)
|
|
}
|
|
if s3Client.Path != "path/to/db" {
|
|
t.Errorf("expected path 'path/to/db', got %q", s3Client.Path)
|
|
}
|
|
})
|
|
|
|
t.Run("S3WithQueryParams", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://mybucket/db?endpoint=localhost:9000®ion=us-west-2")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3Client, ok := client.(*s3.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *s3.ReplicaClient, got %T", client)
|
|
}
|
|
if s3Client.Endpoint != "http://localhost:9000" {
|
|
t.Errorf("expected endpoint 'http://localhost:9000', got %q", s3Client.Endpoint)
|
|
}
|
|
if s3Client.Region != "us-west-2" {
|
|
t.Errorf("expected region 'us-west-2', got %q", s3Client.Region)
|
|
}
|
|
})
|
|
|
|
t.Run("File", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("file:///tmp/replica")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client.Type() != "file" {
|
|
t.Errorf("expected type 'file', got %q", client.Type())
|
|
}
|
|
fileClient, ok := client.(*file.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *file.ReplicaClient, got %T", client)
|
|
}
|
|
if fileClient.Path() != "/tmp/replica" {
|
|
t.Errorf("expected path '/tmp/replica', got %q", fileClient.Path())
|
|
}
|
|
})
|
|
|
|
t.Run("SFTP", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("sftp://[email protected]/path")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client.Type() != "sftp" {
|
|
t.Errorf("expected type 'sftp', got %q", client.Type())
|
|
}
|
|
sftpClient, ok := client.(*sftp.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *sftp.ReplicaClient, got %T", client)
|
|
}
|
|
if sftpClient.Host != "host.example.com" {
|
|
t.Errorf("expected host 'host.example.com', got %q", sftpClient.Host)
|
|
}
|
|
if sftpClient.User != "myuser" {
|
|
t.Errorf("expected user 'myuser', got %q", sftpClient.User)
|
|
}
|
|
if sftpClient.Path != "path" {
|
|
t.Errorf("expected path 'path', got %q", sftpClient.Path)
|
|
}
|
|
})
|
|
|
|
t.Run("SFTP_WithPassword", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("sftp://myuser:[email protected]/path")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sftpClient, ok := client.(*sftp.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *sftp.ReplicaClient, got %T", client)
|
|
}
|
|
if sftpClient.User != "myuser" {
|
|
t.Errorf("expected user 'myuser', got %q", sftpClient.User)
|
|
}
|
|
if sftpClient.Password != "secret" {
|
|
t.Errorf("expected password 'secret', got %q", sftpClient.Password)
|
|
}
|
|
})
|
|
|
|
t.Run("SFTP_RequiresUserError", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("sftp://host.example.com/path")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing user")
|
|
}
|
|
})
|
|
|
|
t.Run("SFTP_MissingHost", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("sftp:///path")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing host")
|
|
}
|
|
})
|
|
|
|
t.Run("WebDAV", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("webdav://host.example.com/path")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client.Type() != "webdav" {
|
|
t.Errorf("expected type 'webdav', got %q", client.Type())
|
|
}
|
|
webdavClient, ok := client.(*webdav.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *webdav.ReplicaClient, got %T", client)
|
|
}
|
|
if webdavClient.URL != "http://host.example.com" {
|
|
t.Errorf("expected URL 'http://host.example.com', got %q", webdavClient.URL)
|
|
}
|
|
if webdavClient.Path != "path" {
|
|
t.Errorf("expected path 'path', got %q", webdavClient.Path)
|
|
}
|
|
})
|
|
|
|
t.Run("WebDAVS", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("webdavs://host.example.com/path")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client.Type() != "webdav" {
|
|
t.Errorf("expected type 'webdav', got %q", client.Type())
|
|
}
|
|
webdavClient, ok := client.(*webdav.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *webdav.ReplicaClient, got %T", client)
|
|
}
|
|
if webdavClient.URL != "https://host.example.com" {
|
|
t.Errorf("expected URL 'https://host.example.com', got %q", webdavClient.URL)
|
|
}
|
|
})
|
|
|
|
t.Run("WebDAV_WithCredentials", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("webdav://myuser:[email protected]/path")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
webdavClient, ok := client.(*webdav.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *webdav.ReplicaClient, got %T", client)
|
|
}
|
|
if webdavClient.Username != "myuser" {
|
|
t.Errorf("expected username 'myuser', got %q", webdavClient.Username)
|
|
}
|
|
if webdavClient.Password != "secret" {
|
|
t.Errorf("expected password 'secret', got %q", webdavClient.Password)
|
|
}
|
|
if webdavClient.URL != "http://host.example.com" {
|
|
t.Errorf("expected URL 'http://host.example.com', got %q", webdavClient.URL)
|
|
}
|
|
})
|
|
|
|
t.Run("WebDAVS_WithCredentials", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("webdavs://myuser:[email protected]/path")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
webdavClient, ok := client.(*webdav.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *webdav.ReplicaClient, got %T", client)
|
|
}
|
|
if webdavClient.Username != "myuser" {
|
|
t.Errorf("expected username 'myuser', got %q", webdavClient.Username)
|
|
}
|
|
if webdavClient.Password != "secret" {
|
|
t.Errorf("expected password 'secret', got %q", webdavClient.Password)
|
|
}
|
|
if webdavClient.URL != "https://host.example.com" {
|
|
t.Errorf("expected URL 'https://host.example.com', got %q", webdavClient.URL)
|
|
}
|
|
})
|
|
|
|
t.Run("WebDAV_MissingHost", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("webdav:///path")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing host")
|
|
}
|
|
})
|
|
|
|
t.Run("NATS", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("nats://localhost:4222/mybucket")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client.Type() != "nats" {
|
|
t.Errorf("expected type 'nats', got %q", client.Type())
|
|
}
|
|
natsClient, ok := client.(*nats.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *nats.ReplicaClient, got %T", client)
|
|
}
|
|
if natsClient.URL != "nats://localhost:4222" {
|
|
t.Errorf("expected URL 'nats://localhost:4222', got %q", natsClient.URL)
|
|
}
|
|
if natsClient.BucketName != "mybucket" {
|
|
t.Errorf("expected bucket 'mybucket', got %q", natsClient.BucketName)
|
|
}
|
|
})
|
|
|
|
t.Run("NATS_WithCredentials", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("nats://myuser:secret@localhost:4222/mybucket")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
natsClient, ok := client.(*nats.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *nats.ReplicaClient, got %T", client)
|
|
}
|
|
if natsClient.Username != "myuser" {
|
|
t.Errorf("expected username 'myuser', got %q", natsClient.Username)
|
|
}
|
|
if natsClient.Password != "secret" {
|
|
t.Errorf("expected password 'secret', got %q", natsClient.Password)
|
|
}
|
|
if natsClient.URL != "nats://localhost:4222" {
|
|
t.Errorf("expected URL 'nats://localhost:4222', got %q", natsClient.URL)
|
|
}
|
|
})
|
|
|
|
t.Run("NATS_MissingBucket", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("nats://localhost:4222/")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing bucket")
|
|
}
|
|
})
|
|
|
|
// Note: file:// with empty path returns "." due to path.Clean behavior.
|
|
// This is technically valid but may not be the intended behavior.
|
|
t.Run("File_EmptyPathReturnsDot", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("file://")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fileClient, ok := client.(*file.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *file.ReplicaClient, got %T", client)
|
|
}
|
|
// path.Clean("") returns "." which passes the empty check
|
|
if fileClient.Path() != "." {
|
|
t.Errorf("expected path '.', got %q", fileClient.Path())
|
|
}
|
|
})
|
|
|
|
t.Run("S3_ARN", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://arn:aws:s3:us-east-1:123456789012:accesspoint/db-access/backups")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3Client, ok := client.(*s3.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *s3.ReplicaClient, got %T", client)
|
|
}
|
|
if s3Client.Bucket != "arn:aws:s3:us-east-1:123456789012:accesspoint/db-access" {
|
|
t.Errorf("expected bucket ARN, got %q", s3Client.Bucket)
|
|
}
|
|
if s3Client.Path != "backups" {
|
|
t.Errorf("expected path 'backups', got %q", s3Client.Path)
|
|
}
|
|
})
|
|
|
|
t.Run("S3_ARN_WithQueryParams", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://arn:aws:s3:us-east-1:123456789012:accesspoint/db-access/backups?sign-payload=false")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3Client, ok := client.(*s3.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *s3.ReplicaClient, got %T", client)
|
|
}
|
|
if s3Client.Bucket != "arn:aws:s3:us-east-1:123456789012:accesspoint/db-access" {
|
|
t.Errorf("expected bucket ARN, got %q", s3Client.Bucket)
|
|
}
|
|
if s3Client.Path != "backups" {
|
|
t.Errorf("expected path 'backups', got %q", s3Client.Path)
|
|
}
|
|
if s3Client.SignPayload != false {
|
|
t.Errorf("expected SignPayload=false from query param, got %v", s3Client.SignPayload)
|
|
}
|
|
})
|
|
|
|
t.Run("S3_MissingBucket", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("s3:///path")
|
|
if err == nil {
|
|
t.Fatal("expected error for missing bucket")
|
|
}
|
|
})
|
|
|
|
t.Run("EmptyURL", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("")
|
|
if err == nil {
|
|
t.Fatal("expected error for empty URL")
|
|
}
|
|
})
|
|
|
|
t.Run("UnsupportedScheme", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("unknown://bucket/path")
|
|
if err == nil {
|
|
t.Fatal("expected error for unsupported scheme")
|
|
}
|
|
})
|
|
|
|
t.Run("InvalidURL", func(t *testing.T) {
|
|
_, err := replicate.NewReplicaClientFromURL("not-a-valid-url")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid URL")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReplicaTypeFromURL(t *testing.T) {
|
|
tests := []struct {
|
|
url string
|
|
expected string
|
|
}{
|
|
{"s3://bucket/path", "s3"},
|
|
{"file:///path/to/replica", "file"},
|
|
{"sftp://host/path", "sftp"},
|
|
{"webdav://host/path", "webdav"},
|
|
{"webdavs://host/path", "webdav"},
|
|
{"nats://host/bucket", "nats"},
|
|
{"", ""},
|
|
{"invalid", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.url, func(t *testing.T) {
|
|
got := replicate.ReplicaTypeFromURL(tt.url)
|
|
if got != tt.expected {
|
|
t.Errorf("ReplicaTypeFromURL(%q) = %q, want %q", tt.url, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsURL(t *testing.T) {
|
|
tests := []struct {
|
|
s string
|
|
expected bool
|
|
}{
|
|
{"s3://bucket/path", true},
|
|
{"file:///path", true},
|
|
{"https://example.com", true},
|
|
{"/path/to/file", false},
|
|
{"relative/path", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.s, func(t *testing.T) {
|
|
got := replicate.IsURL(tt.s)
|
|
if got != tt.expected {
|
|
t.Errorf("IsURL(%q) = %v, want %v", tt.s, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBoolQueryValue(t *testing.T) {
|
|
t.Run("True values", func(t *testing.T) {
|
|
for _, v := range []string{"true", "True", "TRUE", "1", "t", "yes"} {
|
|
query := make(map[string][]string)
|
|
query["key"] = []string{v}
|
|
value, ok := replicate.BoolQueryValue(query, "key")
|
|
if !ok {
|
|
t.Errorf("BoolQueryValue with %q should be ok", v)
|
|
}
|
|
if !value {
|
|
t.Errorf("BoolQueryValue with %q should be true", v)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("False values", func(t *testing.T) {
|
|
for _, v := range []string{"false", "False", "FALSE", "0", "f", "no"} {
|
|
query := make(map[string][]string)
|
|
query["key"] = []string{v}
|
|
value, ok := replicate.BoolQueryValue(query, "key")
|
|
if !ok {
|
|
t.Errorf("BoolQueryValue with %q should be ok", v)
|
|
}
|
|
if value {
|
|
t.Errorf("BoolQueryValue with %q should be false", v)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("Missing key", func(t *testing.T) {
|
|
query := make(map[string][]string)
|
|
_, ok := replicate.BoolQueryValue(query, "key")
|
|
if ok {
|
|
t.Error("BoolQueryValue with missing key should not be ok")
|
|
}
|
|
})
|
|
|
|
t.Run("Multiple keys", func(t *testing.T) {
|
|
query := make(map[string][]string)
|
|
query["key2"] = []string{"true"}
|
|
value, ok := replicate.BoolQueryValue(query, "key1", "key2")
|
|
if !ok {
|
|
t.Error("BoolQueryValue should find second key")
|
|
}
|
|
if !value {
|
|
t.Error("BoolQueryValue should return true for second key")
|
|
}
|
|
})
|
|
|
|
t.Run("Nil query", func(t *testing.T) {
|
|
_, ok := replicate.BoolQueryValue(nil, "key")
|
|
if ok {
|
|
t.Error("BoolQueryValue with nil query should not be ok")
|
|
}
|
|
})
|
|
|
|
t.Run("Invalid value returns false with ok", func(t *testing.T) {
|
|
query := make(map[string][]string)
|
|
query["key"] = []string{"invalid"}
|
|
value, ok := replicate.BoolQueryValue(query, "key")
|
|
if !ok {
|
|
t.Error("BoolQueryValue with invalid value should be ok")
|
|
}
|
|
if value {
|
|
t.Error("BoolQueryValue with invalid value should be false")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestIsTigrisEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"fly.storage.tigris.dev", true},
|
|
{"FLY.STORAGE.TIGRIS.DEV", true},
|
|
{"https://fly.storage.tigris.dev", true},
|
|
{"http://fly.storage.tigris.dev", true},
|
|
{"t3.storage.dev", true},
|
|
{"T3.STORAGE.DEV", true},
|
|
{"https://t3.storage.dev", true},
|
|
{"http://t3.storage.dev", true},
|
|
{"s3.amazonaws.com", false},
|
|
{"localhost:9000", false},
|
|
{"", false},
|
|
{" ", false},
|
|
{"https://s3.us-east-1.amazonaws.com", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsTigrisEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsTigrisEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegionFromS3ARN(t *testing.T) {
|
|
tests := []struct {
|
|
arn string
|
|
expected string
|
|
}{
|
|
{"arn:aws:s3:us-east-1:123456789012:accesspoint/db-access", "us-east-1"},
|
|
{"arn:aws:s3:eu-west-1:123456789012:accesspoint/db-access", "eu-west-1"},
|
|
{"arn:aws:s3:ap-southeast-2:123456789012:accesspoint/db-access", "ap-southeast-2"},
|
|
{"arn:aws:s3::123456789012:accesspoint/db-access", ""},
|
|
{"invalid-arn", ""},
|
|
{"", ""},
|
|
{"arn:aws:s3", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.arn, func(t *testing.T) {
|
|
got := replicate.RegionFromS3ARN(tt.arn)
|
|
if got != tt.expected {
|
|
t.Errorf("RegionFromS3ARN(%q) = %q, want %q", tt.arn, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanReplicaURLPath(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
expected string
|
|
}{
|
|
{"", ""},
|
|
{"path", "path"},
|
|
{"/path", "path"},
|
|
{"path/", "path"},
|
|
{"/path/", "path"},
|
|
{"path/to/db", "path/to/db"},
|
|
{"/path/to/db", "path/to/db"},
|
|
{"//path//to//db", "path/to/db"},
|
|
{".", ""},
|
|
{"/.", ""},
|
|
{"./path", "path"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.path, func(t *testing.T) {
|
|
got := replicate.CleanReplicaURLPath(tt.path)
|
|
if got != tt.expected {
|
|
t.Errorf("CleanReplicaURLPath(%q) = %q, want %q", tt.path, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseS3AccessPointURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
wantScheme string
|
|
wantHost string
|
|
wantPath string
|
|
wantQuery map[string]string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "BasicARN",
|
|
url: "s3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "",
|
|
wantQuery: nil,
|
|
},
|
|
{
|
|
name: "ARNWithPath",
|
|
url: "s3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point/backups/db",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "backups/db",
|
|
wantQuery: nil,
|
|
},
|
|
{
|
|
name: "ARNWithSingleQueryParam",
|
|
url: "s3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point?sign-payload=true",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "",
|
|
wantQuery: map[string]string{"sign-payload": "true"},
|
|
},
|
|
{
|
|
name: "ARNWithMultipleQueryParams",
|
|
url: "s3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point?sign-payload=false®ion=us-west-2",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "",
|
|
wantQuery: map[string]string{"sign-payload": "false", "region": "us-west-2"},
|
|
},
|
|
{
|
|
name: "ARNWithPathAndQuery",
|
|
url: "s3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point/backups?sign-payload=true",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "backups",
|
|
wantQuery: map[string]string{"sign-payload": "true"},
|
|
},
|
|
{
|
|
name: "CaseInsensitiveScheme",
|
|
url: "S3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "",
|
|
wantQuery: nil,
|
|
},
|
|
{
|
|
name: "EmptyQueryValue",
|
|
url: "s3://arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point?key=",
|
|
wantScheme: "s3",
|
|
wantHost: "arn:aws:s3:us-east-1:123456789012:accesspoint/my-access-point",
|
|
wantPath: "",
|
|
wantQuery: map[string]string{"key": ""},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
scheme, host, path, query, _, err := replicate.ParseReplicaURLWithQuery(tt.url)
|
|
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if scheme != tt.wantScheme {
|
|
t.Errorf("scheme = %q, want %q", scheme, tt.wantScheme)
|
|
}
|
|
if host != tt.wantHost {
|
|
t.Errorf("host = %q, want %q", host, tt.wantHost)
|
|
}
|
|
if path != tt.wantPath {
|
|
t.Errorf("path = %q, want %q", path, tt.wantPath)
|
|
}
|
|
|
|
if tt.wantQuery == nil {
|
|
if len(query) > 0 {
|
|
t.Errorf("query = %v, want nil/empty", query)
|
|
}
|
|
} else {
|
|
for key, wantVal := range tt.wantQuery {
|
|
if gotVal := query.Get(key); gotVal != wantVal {
|
|
t.Errorf("query[%q] = %q, want %q", key, gotVal, wantVal)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsDigitalOceanEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"https://sfo3.digitaloceanspaces.com", true},
|
|
{"https://nyc3.digitaloceanspaces.com", true},
|
|
{"sfo3.digitaloceanspaces.com", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsDigitalOceanEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsDigitalOceanEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsBackblazeEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"https://s3.us-west-002.backblazeb2.com", true},
|
|
{"https://s3.eu-central-003.backblazeb2.com", true},
|
|
{"s3.us-west-002.backblazeb2.com", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsBackblazeEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsBackblazeEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsFilebaseEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"https://s3.filebase.com", true},
|
|
{"http://s3.filebase.com", true},
|
|
{"s3.filebase.com", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://sfo3.digitaloceanspaces.com", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsFilebaseEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsFilebaseEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsScalewayEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"https://s3.fr-par.scw.cloud", true},
|
|
{"https://s3.nl-ams.scw.cloud", true},
|
|
{"s3.fr-par.scw.cloud", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsScalewayEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsScalewayEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsCloudflareR2Endpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"https://abcdef123456.r2.cloudflarestorage.com", true},
|
|
{"https://account-id.r2.cloudflarestorage.com", true},
|
|
{"abcdef123456.r2.cloudflarestorage.com", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsCloudflareR2Endpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsCloudflareR2Endpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSupabaseEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"https://myproject.supabase.co/storage/v1/s3", true},
|
|
{"https://abcdefghij.supabase.co", true},
|
|
{"myproject.supabase.co", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsSupabaseEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsSupabaseEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsHetznerEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"fsn1.your-objectstorage.com", true},
|
|
{"nbg1.your-objectstorage.com", true},
|
|
{"https://fsn1.your-objectstorage.com", true},
|
|
{"http://nbg1.your-objectstorage.com", true},
|
|
{"s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsHetznerEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsHetznerEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsSelfHostedS3Endpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
{"http://localhost:9000", true},
|
|
{"http://192.168.1.100:9000", true},
|
|
{"s3.local:9000", true},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"https://s3.filebase.com", false},
|
|
{"https://sfo3.digitaloceanspaces.com", false},
|
|
{"s3.filebase.com", false}, // No port, not self-hosted
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsSelfHostedS3Endpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsSelfHostedS3Endpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsLocalEndpoint(t *testing.T) {
|
|
tests := []struct {
|
|
endpoint string
|
|
expected bool
|
|
}{
|
|
// Localhost variants
|
|
{"localhost", true},
|
|
{"localhost:9000", true},
|
|
{"http://localhost:9000", true},
|
|
{"https://localhost:9000", true},
|
|
|
|
// Loopback IP
|
|
{"127.0.0.1", true},
|
|
{"127.0.0.1:9000", true},
|
|
{"http://127.0.0.1:9000", true},
|
|
|
|
// Private network ranges (RFC1918)
|
|
{"192.168.1.100", true},
|
|
{"192.168.1.100:9000", true},
|
|
{"http://192.168.1.100:9000", true},
|
|
{"10.0.0.1", true},
|
|
{"10.0.0.1:9000", true},
|
|
{"172.16.0.1", true},
|
|
{"172.31.255.255", true},
|
|
|
|
// .local and .localhost TLDs
|
|
{"s3.local", true},
|
|
{"s3.local:9000", true},
|
|
{"http://s3.local:9000", true},
|
|
{"dev.localhost", true},
|
|
{"test.localhost:8080", true},
|
|
|
|
// Non-local endpoints (cloud providers)
|
|
{"s3.amazonaws.com", false},
|
|
{"https://s3.amazonaws.com", false},
|
|
{"abcdef.r2.cloudflarestorage.com", false},
|
|
{"https://abcdef.r2.cloudflarestorage.com", false},
|
|
{"fly.storage.tigris.dev", false},
|
|
{"s3.us-west-000.backblazeb2.com", false},
|
|
{"sfo3.digitaloceanspaces.com", false},
|
|
{"s3.filebase.com", false},
|
|
|
|
// Empty
|
|
{"", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.endpoint, func(t *testing.T) {
|
|
got := replicate.IsLocalEndpoint(tt.endpoint)
|
|
if got != tt.expected {
|
|
t.Errorf("IsLocalEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestS3ProviderDefaults tests that provider-specific defaults are applied
|
|
// when creating S3 clients from URLs with provider endpoints.
|
|
// These tests ensure edge case bugs like #912, #918, #940, #947 don't regress.
|
|
func TestS3ProviderDefaults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
url string
|
|
wantSignPayload bool
|
|
wantForcePathStyle bool
|
|
wantRequireMD5 bool
|
|
}{
|
|
{
|
|
name: "CloudflareR2_SignPayload",
|
|
url: "s3://mybucket/path?endpoint=https://account123.r2.cloudflarestorage.com",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true, // Custom endpoint default
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "BackblazeB2_SignPayloadAndPathStyle",
|
|
url: "s3://mybucket/path?endpoint=https://s3.us-west-002.backblazeb2.com",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true,
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "DigitalOcean_SignPayload",
|
|
url: "s3://mybucket/path?endpoint=https://sfo3.digitaloceanspaces.com",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true, // Custom endpoint default
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "Scaleway_SignPayload",
|
|
url: "s3://mybucket/path?endpoint=https://s3.fr-par.scw.cloud",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true, // Custom endpoint default
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "Filebase_SignPayloadAndPathStyle",
|
|
url: "s3://mybucket/path?endpoint=https://s3.filebase.com",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true,
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "Tigris_SignPayloadNoMD5",
|
|
url: "s3://mybucket/path?endpoint=https://fly.storage.tigris.dev",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true, // Custom endpoint default
|
|
wantRequireMD5: false,
|
|
},
|
|
{
|
|
name: "SelfHosted_SignPayloadAndPathStyle",
|
|
url: "s3://mybucket/path?endpoint=http://localhost:9000",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true,
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "Hetzner_SignPayload",
|
|
url: "s3://mybucket/path?endpoint=https://fsn1.your-objectstorage.com",
|
|
wantSignPayload: true,
|
|
wantForcePathStyle: true,
|
|
wantRequireMD5: true,
|
|
},
|
|
{
|
|
name: "AWS_Defaults",
|
|
url: "s3://mybucket/path",
|
|
wantSignPayload: true, // Default
|
|
wantForcePathStyle: false,
|
|
wantRequireMD5: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL(tt.url)
|
|
if err != nil {
|
|
t.Fatalf("NewReplicaClientFromURL(%q) error: %v", tt.url, err)
|
|
}
|
|
|
|
s3Client, ok := client.(*s3.ReplicaClient)
|
|
if !ok {
|
|
t.Fatalf("expected *s3.ReplicaClient, got %T", client)
|
|
}
|
|
|
|
if s3Client.SignPayload != tt.wantSignPayload {
|
|
t.Errorf("SignPayload = %v, want %v", s3Client.SignPayload, tt.wantSignPayload)
|
|
}
|
|
if s3Client.ForcePathStyle != tt.wantForcePathStyle {
|
|
t.Errorf("ForcePathStyle = %v, want %v", s3Client.ForcePathStyle, tt.wantForcePathStyle)
|
|
}
|
|
if s3Client.RequireContentMD5 != tt.wantRequireMD5 {
|
|
t.Errorf("RequireContentMD5 = %v, want %v", s3Client.RequireContentMD5, tt.wantRequireMD5)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnsureEndpointScheme(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
schemeAdded bool
|
|
}{
|
|
// Already has scheme - no change
|
|
{"https://example.com", "https://example.com", false},
|
|
{"http://localhost:9000", "http://localhost:9000", false},
|
|
{"http://192.168.1.1:9000", "http://192.168.1.1:9000", false},
|
|
|
|
// Local endpoints get http://
|
|
{"localhost:9000", "http://localhost:9000", true},
|
|
{"127.0.0.1:9000", "http://127.0.0.1:9000", true},
|
|
{"192.168.1.100:9000", "http://192.168.1.100:9000", true},
|
|
{"10.0.0.1:9000", "http://10.0.0.1:9000", true},
|
|
{"s3.local:9000", "http://s3.local:9000", true},
|
|
|
|
// Cloud endpoints get https:// (THIS IS THE KEY FIX)
|
|
{"abcdef.r2.cloudflarestorage.com", "https://abcdef.r2.cloudflarestorage.com", true},
|
|
{"s3.us-west-000.backblazeb2.com", "https://s3.us-west-000.backblazeb2.com", true},
|
|
{"fly.storage.tigris.dev", "https://fly.storage.tigris.dev", true},
|
|
{"sfo3.digitaloceanspaces.com", "https://sfo3.digitaloceanspaces.com", true},
|
|
{"s3.filebase.com", "https://s3.filebase.com", true},
|
|
{"s3.fr-par.scw.cloud", "https://s3.fr-par.scw.cloud", true},
|
|
|
|
// Empty returns empty
|
|
{"", "", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
got, added := replicate.EnsureEndpointScheme(tt.input)
|
|
if got != tt.expected {
|
|
t.Errorf("EnsureEndpointScheme(%q) = %q, want %q", tt.input, got, tt.expected)
|
|
}
|
|
if added != tt.schemeAdded {
|
|
t.Errorf("EnsureEndpointScheme(%q) schemeAdded = %v, want %v", tt.input, added, tt.schemeAdded)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestS3ProviderDefaults_QueryParamOverrides tests that explicit query parameters
|
|
// override provider-specific defaults.
|
|
func TestS3ProviderDefaults_QueryParamOverrides(t *testing.T) {
|
|
t.Run("SignPayload_ExplicitFalse", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://mybucket/path?endpoint=https://account123.r2.cloudflarestorage.com&sign-payload=false")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3Client := client.(*s3.ReplicaClient)
|
|
if s3Client.SignPayload != false {
|
|
t.Errorf("SignPayload = %v, want false (explicit override)", s3Client.SignPayload)
|
|
}
|
|
})
|
|
|
|
t.Run("ForcePathStyle_ExplicitFalse", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://mybucket/path?endpoint=https://s3.us-west-002.backblazeb2.com&forcePathStyle=false")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3Client := client.(*s3.ReplicaClient)
|
|
if s3Client.ForcePathStyle != false {
|
|
t.Errorf("ForcePathStyle = %v, want false (explicit override)", s3Client.ForcePathStyle)
|
|
}
|
|
})
|
|
|
|
t.Run("RequireMD5_ExplicitTrue_Tigris", func(t *testing.T) {
|
|
client, err := replicate.NewReplicaClientFromURL("s3://mybucket/path?endpoint=https://fly.storage.tigris.dev&require-content-md5=true")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
s3Client := client.(*s3.ReplicaClient)
|
|
if s3Client.RequireContentMD5 != true {
|
|
t.Errorf("RequireContentMD5 = %v, want true (explicit override)", s3Client.RequireContentMD5)
|
|
}
|
|
})
|
|
}
|