Files
litestream/replica_url_test.go
Ben Johnson fa0ba8efc7 Add alternate Tigris endpoint (#906)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 13:42:40 -07:00

771 lines
22 KiB
Go

package litestream_test
import (
"testing"
"github.com/benbjohnson/litestream"
"github.com/benbjohnson/litestream/abs"
"github.com/benbjohnson/litestream/file"
"github.com/benbjohnson/litestream/gs"
"github.com/benbjohnson/litestream/nats"
"github.com/benbjohnson/litestream/oss"
"github.com/benbjohnson/litestream/s3"
"github.com/benbjohnson/litestream/sftp"
"github.com/benbjohnson/litestream/webdav"
)
func TestNewReplicaClientFromURL(t *testing.T) {
t.Run("S3", func(t *testing.T) {
client, err := litestream.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 := litestream.NewReplicaClientFromURL("s3://mybucket/db?endpoint=localhost:9000&region=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 := litestream.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("GS", func(t *testing.T) {
client, err := litestream.NewReplicaClientFromURL("gs://mybucket/path")
if err != nil {
t.Fatal(err)
}
if client.Type() != "gs" {
t.Errorf("expected type 'gs', got %q", client.Type())
}
gsClient, ok := client.(*gs.ReplicaClient)
if !ok {
t.Fatalf("expected *gs.ReplicaClient, got %T", client)
}
if gsClient.Bucket != "mybucket" {
t.Errorf("expected bucket 'mybucket', got %q", gsClient.Bucket)
}
if gsClient.Path != "path" {
t.Errorf("expected path 'path', got %q", gsClient.Path)
}
})
t.Run("GS_MissingBucket", func(t *testing.T) {
_, err := litestream.NewReplicaClientFromURL("gs:///path")
if err == nil {
t.Fatal("expected error for missing bucket")
}
})
t.Run("ABS", func(t *testing.T) {
client, err := litestream.NewReplicaClientFromURL("abs://mycontainer/path")
if err != nil {
t.Fatal(err)
}
if client.Type() != "abs" {
t.Errorf("expected type 'abs', got %q", client.Type())
}
absClient, ok := client.(*abs.ReplicaClient)
if !ok {
t.Fatalf("expected *abs.ReplicaClient, got %T", client)
}
if absClient.Bucket != "mycontainer" {
t.Errorf("expected bucket 'mycontainer', got %q", absClient.Bucket)
}
if absClient.Path != "path" {
t.Errorf("expected path 'path', got %q", absClient.Path)
}
})
t.Run("ABS_WithAccount", func(t *testing.T) {
client, err := litestream.NewReplicaClientFromURL("abs://myaccount@mycontainer/path")
if err != nil {
t.Fatal(err)
}
absClient, ok := client.(*abs.ReplicaClient)
if !ok {
t.Fatalf("expected *abs.ReplicaClient, got %T", client)
}
if absClient.AccountName != "myaccount" {
t.Errorf("expected account 'myaccount', got %q", absClient.AccountName)
}
if absClient.Bucket != "mycontainer" {
t.Errorf("expected bucket 'mycontainer', got %q", absClient.Bucket)
}
})
t.Run("ABS_MissingBucket", func(t *testing.T) {
_, err := litestream.NewReplicaClientFromURL("abs:///path")
if err == nil {
t.Fatal("expected error for missing bucket")
}
})
t.Run("SFTP", func(t *testing.T) {
client, err := litestream.NewReplicaClientFromURL("sftp://myuser@host.example.com/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 := litestream.NewReplicaClientFromURL("sftp://myuser:secret@host.example.com/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 := litestream.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 := litestream.NewReplicaClientFromURL("sftp:///path")
if err == nil {
t.Fatal("expected error for missing host")
}
})
t.Run("WebDAV", func(t *testing.T) {
client, err := litestream.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 := litestream.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 := litestream.NewReplicaClientFromURL("webdav://myuser:secret@host.example.com/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 := litestream.NewReplicaClientFromURL("webdavs://myuser:secret@host.example.com/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 := litestream.NewReplicaClientFromURL("webdav:///path")
if err == nil {
t.Fatal("expected error for missing host")
}
})
t.Run("NATS", func(t *testing.T) {
client, err := litestream.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 := litestream.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 := litestream.NewReplicaClientFromURL("nats://localhost:4222/")
if err == nil {
t.Fatal("expected error for missing bucket")
}
})
t.Run("OSS", func(t *testing.T) {
client, err := litestream.NewReplicaClientFromURL("oss://mybucket/path")
if err != nil {
t.Fatal(err)
}
if client.Type() != "oss" {
t.Errorf("expected type 'oss', got %q", client.Type())
}
ossClient, ok := client.(*oss.ReplicaClient)
if !ok {
t.Fatalf("expected *oss.ReplicaClient, got %T", client)
}
if ossClient.Bucket != "mybucket" {
t.Errorf("expected bucket 'mybucket', got %q", ossClient.Bucket)
}
if ossClient.Path != "path" {
t.Errorf("expected path 'path', got %q", ossClient.Path)
}
})
t.Run("OSS_WithRegion", func(t *testing.T) {
client, err := litestream.NewReplicaClientFromURL("oss://mybucket.oss-cn-shanghai.aliyuncs.com/path")
if err != nil {
t.Fatal(err)
}
ossClient, ok := client.(*oss.ReplicaClient)
if !ok {
t.Fatalf("expected *oss.ReplicaClient, got %T", client)
}
if ossClient.Bucket != "mybucket" {
t.Errorf("expected bucket 'mybucket', got %q", ossClient.Bucket)
}
// Note: Region is extracted without the 'oss-' prefix
if ossClient.Region != "cn-shanghai" {
t.Errorf("expected region 'cn-shanghai', got %q", ossClient.Region)
}
})
t.Run("OSS_MissingBucket", func(t *testing.T) {
_, err := litestream.NewReplicaClientFromURL("oss:///path")
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 := litestream.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 := litestream.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_MissingBucket", func(t *testing.T) {
_, err := litestream.NewReplicaClientFromURL("s3:///path")
if err == nil {
t.Fatal("expected error for missing bucket")
}
})
t.Run("EmptyURL", func(t *testing.T) {
_, err := litestream.NewReplicaClientFromURL("")
if err == nil {
t.Fatal("expected error for empty URL")
}
})
t.Run("UnsupportedScheme", func(t *testing.T) {
_, err := litestream.NewReplicaClientFromURL("unknown://bucket/path")
if err == nil {
t.Fatal("expected error for unsupported scheme")
}
})
t.Run("InvalidURL", func(t *testing.T) {
_, err := litestream.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"},
{"gs://bucket/path", "gs"},
{"abs://container/path", "abs"},
{"file:///path/to/replica", "file"},
{"sftp://host/path", "sftp"},
{"webdav://host/path", "webdav"},
{"webdavs://host/path", "webdav"},
{"nats://host/bucket", "nats"},
{"oss://bucket/path", "oss"},
{"", ""},
{"invalid", ""},
}
for _, tt := range tests {
t.Run(tt.url, func(t *testing.T) {
got := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.CleanReplicaURLPath(tt.path)
if got != tt.expected {
t.Errorf("CleanReplicaURLPath(%q) = %q, want %q", tt.path, got, tt.expected)
}
})
}
}
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 := litestream.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 := litestream.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 := litestream.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 := litestream.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 := litestream.IsCloudflareR2Endpoint(tt.endpoint)
if got != tt.expected {
t.Errorf("IsCloudflareR2Endpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
}
})
}
}
func TestIsMinIOEndpoint(t *testing.T) {
tests := []struct {
endpoint string
expected bool
}{
{"http://localhost:9000", true},
{"http://192.168.1.100:9000", true},
{"minio.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 MinIO
{"", false},
}
for _, tt := range tests {
t.Run(tt.endpoint, func(t *testing.T) {
got := litestream.IsMinIOEndpoint(tt.endpoint)
if got != tt.expected {
t.Errorf("IsMinIOEndpoint(%q) = %v, want %v", tt.endpoint, got, tt.expected)
}
})
}
}