Files

1391 lines
35 KiB
Go
Raw Permalink Normal View History

//go:build linux || freebsd
// +build linux freebsd
package protocol_test
import (
"context"
"net/url"
"testing"
"codeberg.org/petrbalvin/goget/internal/core"
"codeberg.org/petrbalvin/goget/internal/protocol"
)
// ---------- Mock Protocol Implementation ----------
type mockProtocol struct {
*protocol.BaseProtocol
canHandleFn func(u *url.URL) bool
}
func (m *mockProtocol) CanHandle(u *url.URL) bool {
if m.canHandleFn != nil {
return m.canHandleFn(u)
}
return true
}
func (m *mockProtocol) Download(ctx context.Context, req *core.DownloadRequest) (*core.DownloadResult, error) {
return nil, nil
}
func (m *mockProtocol) SupportsParallel() bool {
return false
}
func (m *mockProtocol) SupportsRecursive() bool {
return false
}
// ---------- ProtocolInfo Tests ----------
func TestProtocolInfo(t *testing.T) {
t.Run("creates with all fields", func(t *testing.T) {
info := protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
Version: "1.1",
Operations: []string{"download", "upload"},
Features: []string{"resume", "compression", "range"},
DefaultPort: 80,
}
if info.Name != "HTTP" {
t.Errorf("Name = %q, want %q", info.Name, "HTTP")
}
if info.Scheme != "http" {
t.Errorf("Scheme = %q, want %q", info.Scheme, "http")
}
if info.Version != "1.1" {
t.Errorf("Version = %q, want %q", info.Version, "1.1")
}
if len(info.Operations) != 2 {
t.Errorf("len(Operations) = %d, want %d", len(info.Operations), 2)
}
if len(info.Features) != 3 {
t.Errorf("len(Features) = %d, want %d", len(info.Features), 3)
}
if info.DefaultPort != 80 {
t.Errorf("DefaultPort = %d, want %d", info.DefaultPort, 80)
}
})
t.Run("empty ProtocolInfo", func(t *testing.T) {
info := protocol.ProtocolInfo{}
if info.Name != "" {
t.Errorf("Name = %q, want %q", info.Name, "")
}
if info.Scheme != "" {
t.Errorf("Scheme = %q, want %q", info.Scheme, "")
}
if info.DefaultPort != 0 {
t.Errorf("DefaultPort = %d, want %d", info.DefaultPort, 0)
}
if len(info.Operations) != 0 {
t.Errorf("len(Operations) = %d, want %d", len(info.Operations), 0)
}
if len(info.Features) != 0 {
t.Errorf("len(Features) = %d, want %d", len(info.Features), 0)
}
})
t.Run("with features subset", func(t *testing.T) {
info := protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
Features: []string{"upload"},
}
if info.Name != "FTP" {
t.Errorf("Name = %q, want %q", info.Name, "FTP")
}
if len(info.Features) != 1 || info.Features[0] != "upload" {
t.Errorf("Features = %v, want [upload]", info.Features)
}
})
}
// ---------- NewBaseProtocol Tests ----------
func TestNewBaseProtocol(t *testing.T) {
t.Run("creates with valid info", func(t *testing.T) {
info := protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
Version: "1.1",
Features: []string{"resume", "compression"},
}
bp := protocol.NewBaseProtocol(info)
if bp == nil {
t.Fatal("NewBaseProtocol returned nil")
}
})
t.Run("creates with empty info", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
if bp == nil {
t.Fatal("NewBaseProtocol with empty info returned nil")
}
})
t.Run("preserves info fields", func(t *testing.T) {
info := protocol.ProtocolInfo{
Name: "SFTP",
Scheme: "sftp",
Version: "3.0",
DefaultPort: 22,
Operations: []string{"download", "upload"},
Features: []string{"resume", "compression", "range", "upload"},
}
bp := protocol.NewBaseProtocol(info)
if bp.Scheme() != info.Scheme {
t.Errorf("Scheme() = %q, want %q", bp.Scheme(), info.Scheme)
}
if bp.Name() != info.Name {
t.Errorf("Name() = %q, want %q", bp.Name(), info.Name)
}
})
}
// ---------- BaseProtocol Scheme Tests ----------
func TestBaseProtocolScheme(t *testing.T) {
tests := []struct {
name string
scheme string
expected string
}{
{"http scheme", "http", "http"},
{"https scheme", "https", "https"},
{"ftp scheme", "ftp", "ftp"},
{"sftp scheme", "sftp", "sftp"},
{"empty scheme", "", ""},
{"custom scheme", "custom", "custom"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{Scheme: tt.scheme})
got := bp.Scheme()
if got != tt.expected {
t.Errorf("Scheme() = %q, want %q", got, tt.expected)
}
})
}
}
// ---------- BaseProtocol Name Tests ----------
func TestBaseProtocolName(t *testing.T) {
tests := []struct {
name string
protName string
expected string
}{
{"HTTP name", "HTTP", "HTTP"},
{"FTP name", "FTP", "FTP"},
{"empty name", "", ""},
{"with version", "HTTP/1.1", "HTTP/1.1"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{Name: tt.protName})
got := bp.Name()
if got != tt.expected {
t.Errorf("Name() = %q, want %q", got, tt.expected)
}
})
}
}
// ---------- BaseProtocol Capabilities Tests ----------
func TestBaseProtocolCapabilities(t *testing.T) {
t.Run("returns features slice", func(t *testing.T) {
features := []string{"resume", "compression", "range"}
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{Features: features})
caps := bp.Capabilities()
if len(caps) != len(features) {
t.Errorf("len(Capabilities()) = %d, want %d", len(caps), len(features))
}
for i, f := range features {
if caps[i] != f {
t.Errorf("Capabilities()[%d] = %q, want %q", i, caps[i], f)
}
}
})
t.Run("returns empty slice for no features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
caps := bp.Capabilities()
if len(caps) != 0 {
t.Errorf("len(Capabilities()) = %d, want %d", len(caps), 0)
}
})
t.Run("returns slice reflecting ProtocolInfo.Features", func(t *testing.T) {
features := []string{"resume", "compression"}
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{Features: features})
caps := bp.Capabilities()
if len(caps) != len(features) {
t.Fatalf("len(Capabilities()) = %d, want %d", len(caps), len(features))
}
for i, f := range features {
if caps[i] != f {
t.Errorf("Capabilities()[%d] = %q, want %q", i, caps[i], f)
}
}
})
}
// ---------- BaseProtocol SupportsResume Tests ----------
func TestBaseProtocolSupportsResume(t *testing.T) {
t.Run("with resume feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume"},
})
if !bp.SupportsResume() {
t.Error("SupportsResume() = false, want true")
}
})
t.Run("without resume feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"compression", "range"},
})
if bp.SupportsResume() {
t.Error("SupportsResume() = true, want false")
}
})
t.Run("with empty features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
if bp.SupportsResume() {
t.Error("SupportsResume() = true, want false")
}
})
t.Run("resume is first feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume", "compression", "range"},
})
if !bp.SupportsResume() {
t.Error("SupportsResume() = false, want true")
}
})
t.Run("resume is last feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"compression", "range", "upload", "resume"},
})
if !bp.SupportsResume() {
t.Error("SupportsResume() = false, want true")
}
})
}
// ---------- BaseProtocol SupportsCompression Tests ----------
func TestBaseProtocolSupportsCompression(t *testing.T) {
t.Run("with compression feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"compression"},
})
if !bp.SupportsCompression() {
t.Error("SupportsCompression() = false, want true")
}
})
t.Run("without compression feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume", "range"},
})
if bp.SupportsCompression() {
t.Error("SupportsCompression() = true, want false")
}
})
t.Run("with empty features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
if bp.SupportsCompression() {
t.Error("SupportsCompression() = true, want false")
}
})
}
// ---------- BaseProtocol SupportsUpload Tests ----------
func TestBaseProtocolSupportsUpload(t *testing.T) {
t.Run("with upload feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"upload"},
})
if !bp.SupportsUpload() {
t.Error("SupportsUpload() = false, want true")
}
})
t.Run("without upload feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume", "compression"},
})
if bp.SupportsUpload() {
t.Error("SupportsUpload() = true, want false")
}
})
t.Run("with empty features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
if bp.SupportsUpload() {
t.Error("SupportsUpload() = true, want false")
}
})
}
// ---------- BaseProtocol SupportsRange Tests ----------
func TestBaseProtocolSupportsRange(t *testing.T) {
t.Run("with range feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"range"},
})
if !bp.SupportsRange() {
t.Error("SupportsRange() = false, want true")
}
})
t.Run("without range feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume", "compression"},
})
if bp.SupportsRange() {
t.Error("SupportsRange() = true, want false")
}
})
t.Run("with empty features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
if bp.SupportsRange() {
t.Error("SupportsRange() = true, want false")
}
})
}
// ---------- ValidateURL Tests ----------
func TestValidateURL(t *testing.T) {
t.Run("valid URL", func(t *testing.T) {
u, _ := url.Parse("http://example.com/file.txt")
err := protocol.ValidateURL(u, "http")
if err != nil {
t.Errorf("ValidateURL() error = %v, want nil", err)
}
})
t.Run("nil URL", func(t *testing.T) {
err := protocol.ValidateURL(nil, "http")
if err == nil {
t.Fatal("ValidateURL(nil) error = nil, want error")
}
gogetErr, ok := err.(*core.GogetError)
if !ok {
t.Fatalf("error type = %T, want *core.GogetError", err)
}
if gogetErr.Message != "URL is nil" {
t.Errorf("Message = %q, want %q", gogetErr.Message, "URL is nil")
}
})
t.Run("wrong scheme", func(t *testing.T) {
u, _ := url.Parse("ftp://example.com/file.txt")
err := protocol.ValidateURL(u, "http")
if err == nil {
t.Fatal("ValidateURL() for wrong scheme: error = nil, want error")
}
gogetErr, ok := err.(*core.GogetError)
if !ok {
t.Fatalf("error type = %T, want *core.GogetError", err)
}
expectedMsg := "expected scheme http, got ftp"
if gogetErr.Message != expectedMsg {
t.Errorf("Message = %q, want %q", gogetErr.Message, expectedMsg)
}
if gogetErr.URL != "ftp://example.com/file.txt" {
t.Errorf("URL = %q, want %q", gogetErr.URL, "ftp://example.com/file.txt")
}
})
t.Run("missing host", func(t *testing.T) {
u, _ := url.Parse("http:///file.txt")
err := protocol.ValidateURL(u, "http")
if err == nil {
t.Fatal("ValidateURL() for missing host: error = nil, want error")
}
gogetErr, ok := err.(*core.GogetError)
if !ok {
t.Fatalf("error type = %T, want *core.GogetError", err)
}
if gogetErr.Message != "missing host in URL" {
t.Errorf("Message = %q, want %q", gogetErr.Message, "missing host in URL")
}
})
t.Run("URL scheme is lowercased by Go parser", func(t *testing.T) {
u, _ := url.Parse("HTTP://example.com/file.txt")
err := protocol.ValidateURL(u, "http")
if err != nil {
t.Errorf("ValidateURL() error = %v, want nil (Go lowercases the scheme)", err)
}
})
t.Run("URL without path", func(t *testing.T) {
u, _ := url.Parse("http://example.com")
err := protocol.ValidateURL(u, "http")
if err != nil {
t.Errorf("ValidateURL() error = %v, want nil", err)
}
})
t.Run("URL with port", func(t *testing.T) {
u, _ := url.Parse("http://example.com:8080/file.txt")
err := protocol.ValidateURL(u, "http")
if err != nil {
t.Errorf("ValidateURL() error = %v, want nil", err)
}
})
t.Run("URL with query string", func(t *testing.T) {
u, _ := url.Parse("http://example.com/file.txt?token=abc")
err := protocol.ValidateURL(u, "http")
if err != nil {
t.Errorf("ValidateURL() error = %v, want nil", err)
}
})
}
// ---------- GetDefaultPort Tests ----------
func TestGetDefaultPort(t *testing.T) {
tests := []struct {
scheme string
expected int
}{
{"http", 80},
{"https", 443},
{"webdav", 80},
{"webdavs", 443},
{"ftp", 21},
{"ftps", 990},
{"sftp", 22},
{"unknown", 0},
{"gopher", 0},
{"", 0},
{"HTTP", 0},
}
for _, tt := range tests {
t.Run(tt.scheme, func(t *testing.T) {
got := protocol.GetDefaultPort(tt.scheme)
if got != tt.expected {
t.Errorf("GetDefaultPort(%q) = %d, want %d", tt.scheme, got, tt.expected)
}
})
}
}
// ---------- Contains Tests ----------
func TestContains(t *testing.T) {
t.Run("feature present via SupportsResume", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume", "compression"},
})
if !bp.SupportsResume() {
t.Error("SupportsResume() = false, want true")
}
if !bp.SupportsCompression() {
t.Error("SupportsCompression() = false, want true")
}
if bp.SupportsUpload() {
t.Error("SupportsUpload() = true, want false")
}
if bp.SupportsRange() {
t.Error("SupportsRange() = true, want false")
}
})
t.Run("feature not present", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"upload", "range"},
})
if bp.SupportsResume() {
t.Error("SupportsResume() = true, want false")
}
if bp.SupportsCompression() {
t.Error("SupportsCompression() = true, want false")
}
if !bp.SupportsUpload() {
t.Error("SupportsUpload() = false, want true")
}
if !bp.SupportsRange() {
t.Error("SupportsRange() = false, want true")
}
})
t.Run("empty features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{})
if bp.SupportsResume() {
t.Error("SupportsResume() = true, want false")
}
if bp.SupportsCompression() {
t.Error("SupportsCompression() = true, want false")
}
if bp.SupportsUpload() {
t.Error("SupportsUpload() = true, want false")
}
if bp.SupportsRange() {
t.Error("SupportsRange() = true, want false")
}
})
t.Run("single feature", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume"},
})
if !bp.SupportsResume() {
t.Error("SupportsResume() = false, want true")
}
if bp.SupportsCompression() {
t.Error("SupportsCompression() = true, want false")
}
})
t.Run("all features", func(t *testing.T) {
bp := protocol.NewBaseProtocol(protocol.ProtocolInfo{
Features: []string{"resume", "compression", "upload", "range"},
})
if !bp.SupportsResume() {
t.Error("SupportsResume() = false, want true")
}
if !bp.SupportsCompression() {
t.Error("SupportsCompression() = false, want true")
}
if !bp.SupportsUpload() {
t.Error("SupportsUpload() = false, want true")
}
if !bp.SupportsRange() {
t.Error("SupportsRange() = false, want true")
}
})
}
// ---------- Registry Tests ----------
func TestNewRegistry(t *testing.T) {
t.Run("creates empty registry", func(t *testing.T) {
r := protocol.NewRegistry()
if r == nil {
t.Fatal("NewRegistry() returned nil")
}
schemes := r.List()
if len(schemes) != 0 {
t.Errorf("List() returned %v, want empty slice", schemes)
}
})
t.Run("creates independent instances", func(t *testing.T) {
r1 := protocol.NewRegistry()
r2 := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r1.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
schemes := r2.List()
if len(schemes) != 0 {
t.Errorf("New registry should be independent, got %v", schemes)
}
})
}
func TestRegister(t *testing.T) {
t.Run("registers http protocol", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
err := r.Register(httpProto)
if err != nil {
t.Errorf("Register() error = %v, want nil", err)
}
schemes := r.List()
if len(schemes) != 1 || schemes[0] != "http" {
t.Errorf("List() = %v, want [http]", schemes)
}
})
t.Run("registers multiple protocols", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register(http) error = %v", err)
}
if err := r.Register(ftpProto); err != nil {
t.Fatalf("Register(ftp) error = %v", err)
}
schemes := r.List()
if len(schemes) != 2 {
t.Errorf("List() = %v, want [ftp http]", schemes)
}
})
}
func TestRegisterDuplicate(t *testing.T) {
r := protocol.NewRegistry()
httpProto1 := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP/1.1",
Scheme: "http",
}),
}
httpProto2 := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP/2",
Scheme: "http",
}),
}
if err := r.Register(httpProto1); err != nil {
t.Fatalf("First Register() error = %v", err)
}
err := r.Register(httpProto2)
if err == nil {
t.Fatal("Duplicate Register() error = nil, want error")
}
expected := "protocol already registered: http"
if err.Error() != expected {
t.Errorf("Error() = %q, want %q", err.Error(), expected)
}
u, _ := url.Parse("http://example.com/file.txt")
p, err := r.Get(u)
if err != nil {
t.Fatalf("Get() error = %v", err)
}
if p.Scheme() != "http" {
t.Errorf("Protocol scheme = %q, want %q", p.Scheme(), "http")
}
}
func TestRegisterEmptyScheme(t *testing.T) {
r := protocol.NewRegistry()
emptyProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "Empty",
}),
}
err := r.Register(emptyProto)
if err == nil {
t.Fatal("Register() with empty scheme: error = nil, want error")
}
if err.Error() != "protocol scheme cannot be empty" {
t.Errorf("Error() = %q, want %q", err.Error(), "protocol scheme cannot be empty")
}
}
func TestGet(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
t.Run("gets http protocol", func(t *testing.T) {
u, _ := url.Parse("http://example.com/file.txt")
p, err := r.Get(u)
if err != nil {
t.Errorf("Get() error = %v, want nil", err)
}
if p == nil {
t.Fatal("Get() returned nil")
}
if p.Scheme() != "http" {
t.Errorf("Protocol scheme = %q, want %q", p.Scheme(), "http")
}
})
t.Run("gets https via normalization", func(t *testing.T) {
u, _ := url.Parse("https://example.com/file.txt")
p, err := r.Get(u)
if err != nil {
t.Errorf("Get(https) error = %v, want nil", err)
}
if p == nil {
t.Fatal("Get() returned nil")
}
if p.Scheme() != "http" {
t.Errorf("Protocol scheme = %q, want %q", p.Scheme(), "http")
}
})
t.Run("CanHandle returns true", func(t *testing.T) {
u, _ := url.Parse("http://example.com/file.txt")
p, err := r.Get(u)
if err != nil {
t.Errorf("Get() error = %v, want nil", err)
}
if !p.CanHandle(u) {
t.Error("CanHandle() = false, want true")
}
})
t.Run("CanHandle called after normalization", func(t *testing.T) {
r2 := protocol.NewRegistry()
canHandleCalled := false
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
canHandleFn: func(u *url.URL) bool {
canHandleCalled = true
return u.Host != ""
},
}
if err := r2.Register(ftpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
u, _ := url.Parse("ftp://example.com/file.txt")
p, err := r2.Get(u)
if err != nil {
t.Errorf("Get() error = %v, want nil", err)
}
if !canHandleCalled {
t.Error("CanHandle() was not called")
}
if p == nil {
t.Fatal("Get() returned nil")
}
})
t.Run("CanHandle returns false", func(t *testing.T) {
r2 := protocol.NewRegistry()
restrictiveProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "Restrictive",
Scheme: "restrict",
}),
canHandleFn: func(u *url.URL) bool {
return false
},
}
if err := r2.Register(restrictiveProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
u, _ := url.Parse("restrict://example.com/file.txt")
_, err := r2.Get(u)
if err == nil {
t.Fatal("Get() with CanHandle=false: error = nil, want error")
}
})
}
func TestGetUnsupported(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
t.Run("unsupported scheme returns error", func(t *testing.T) {
u, _ := url.Parse("gopher://example.com/file.txt")
_, err := r.Get(u)
if err == nil {
t.Fatal("Get(gopher) error = nil, want error")
}
expected := "unsupported protocol: gopher"
if err.Error() != expected {
t.Errorf("Error() = %q, want %q", err.Error(), expected)
}
})
t.Run("ftps maps to ftp", func(t *testing.T) {
u, _ := url.Parse("ftps://example.com/file.txt")
_, err := r.Get(u)
if err == nil {
t.Fatal("Get(ftps) with only http registered: error = nil, want error")
}
})
}
func TestGetNilURL(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
_, err := r.Get(nil)
if err == nil {
t.Fatal("Get(nil) error = nil, want error")
}
if err.Error() != "url cannot be nil" {
t.Errorf("Error() = %q, want %q", err.Error(), "url cannot be nil")
}
}
func TestList(t *testing.T) {
t.Run("empty registry", func(t *testing.T) {
r := protocol.NewRegistry()
schemes := r.List()
if schemes == nil {
t.Error("List() returned nil, want empty slice")
}
if len(schemes) != 0 {
t.Errorf("List() = %v, want empty", schemes)
}
})
t.Run("single protocol", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
r.Register(httpProto)
schemes := r.List()
if len(schemes) != 1 || schemes[0] != "http" {
t.Errorf("List() = %v, want [http]", schemes)
}
})
t.Run("multiple protocols sorted", func(t *testing.T) {
r := protocol.NewRegistry()
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
}
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
sftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "SFTP",
Scheme: "sftp",
}),
}
r.Register(httpProto)
r.Register(sftpProto)
r.Register(ftpProto)
schemes := r.List()
expected := []string{"ftp", "http", "sftp"}
if len(schemes) != len(expected) {
t.Errorf("List() = %v, want %v", schemes, expected)
return
}
for i, s := range schemes {
if s != expected[i] {
t.Errorf("List()[%d] = %q, want %q", i, s, expected[i])
}
}
})
t.Run("not affected by external modifications", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
r.Register(httpProto)
schemes1 := r.List()
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
}
r.Register(ftpProto)
if len(schemes1) != 1 {
t.Errorf("Previous List() result changed: got %d elements, want %d", len(schemes1), 1)
}
})
}
func TestSupports(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
tests := []struct {
scheme string
want bool
}{
{"http", true},
{"https", true},
{"ftp", false},
{"ftps", false},
{"sftp", false},
{"unknown", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.scheme, func(t *testing.T) {
got := r.Supports(tt.scheme)
if got != tt.want {
t.Errorf("Supports(%q) = %v, want %v", tt.scheme, got, tt.want)
}
})
}
t.Run("supports webdav via normalization", func(t *testing.T) {
r2 := protocol.NewRegistry()
webdavProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "WebDAV",
Scheme: "webdav",
}),
}
r2.Register(webdavProto)
if !r2.Supports("webdav") {
t.Error("Supports(webdav) = false, want true")
}
if !r2.Supports("webdavs") {
t.Error("Supports(webdavs) = false, want true")
}
})
}
func TestCapabilities(t *testing.T) {
t.Run("returns empty map for empty registry", func(t *testing.T) {
r := protocol.NewRegistry()
caps := r.Capabilities()
if caps == nil {
t.Error("Capabilities() returned nil, want empty map")
}
if len(caps) != 0 {
t.Errorf("Capabilities() = %v, want empty map", caps)
}
})
t.Run("returns capabilities for single protocol", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
Features: []string{"resume", "compression", "range"},
}),
}
r.Register(httpProto)
caps := r.Capabilities()
if len(caps) != 1 {
t.Errorf("len(Capabilities()) = %d, want %d", len(caps), 1)
}
features, ok := caps["http"]
if !ok {
t.Fatal("Capabilities() missing http key")
}
expected := []string{"resume", "compression", "range"}
if len(features) != len(expected) {
t.Errorf("features = %v, want %v", features, expected)
}
for i, f := range features {
if f != expected[i] {
t.Errorf("features[%d] = %q, want %q", i, f, expected[i])
}
}
})
t.Run("returns capabilities for multiple protocols", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
Features: []string{"resume", "range"},
}),
}
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
Features: []string{"resume", "upload"},
}),
}
sftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "SFTP",
Scheme: "sftp",
}),
}
r.Register(httpProto)
r.Register(ftpProto)
r.Register(sftpProto)
caps := r.Capabilities()
if len(caps) != 3 {
t.Errorf("len(Capabilities()) = %d, want %d", len(caps), 3)
}
if _, ok := caps["http"]; !ok {
t.Error("Capabilities() missing http key")
}
if _, ok := caps["ftp"]; !ok {
t.Error("Capabilities() missing ftp key")
}
if _, ok := caps["sftp"]; !ok {
t.Error("Capabilities() missing sftp key")
}
if len(caps["sftp"]) != 0 {
t.Errorf("sftp capabilities = %v, want empty", caps["sftp"])
}
})
t.Run("map is independent copy", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
Features: []string{"resume"},
}),
}
r.Register(httpProto)
caps := r.Capabilities()
caps["http"] = []string{"modified"}
caps2 := r.Capabilities()
if len(caps2["http"]) != 1 || caps2["http"][0] != "resume" {
t.Errorf("Capabilities() returned aliased map: got %v, want [resume]", caps2["http"])
}
})
}
// ---------- NormalizeScheme Tests ----------
func TestNormalizeScheme(t *testing.T) {
t.Run("https normalizes to http via Supports", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
if !r.Supports("https") {
t.Error("Supports(https) = false, want true (normalized to http)")
}
})
t.Run("https URL resolves via Get", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
u, _ := url.Parse("https://example.com/file.txt")
p, err := r.Get(u)
if err != nil {
t.Errorf("Get(https) error = %v, want nil", err)
}
if p.Scheme() != "http" {
t.Errorf("scheme = %q, want %q", p.Scheme(), "http")
}
})
t.Run("webdavs normalizes to webdav", func(t *testing.T) {
r := protocol.NewRegistry()
webdavProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "WebDAV",
Scheme: "webdav",
}),
}
if err := r.Register(webdavProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
if !r.Supports("webdav") {
t.Error("Supports(webdav) = false, want true")
}
if !r.Supports("webdavs") {
t.Error("Supports(webdavs) = false, want true (normalized to webdav)")
}
})
t.Run("ftps normalizes to ftp", func(t *testing.T) {
r := protocol.NewRegistry()
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
}
if err := r.Register(ftpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
if !r.Supports("ftp") {
t.Error("Supports(ftp) = false, want true")
}
if !r.Supports("ftps") {
t.Error("Supports(ftps) = false, want true (normalized to ftp)")
}
})
t.Run("ftps URL resolves via Get", func(t *testing.T) {
r := protocol.NewRegistry()
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
}
if err := r.Register(ftpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
u, _ := url.Parse("ftps://example.com/file.txt")
p, err := r.Get(u)
if err != nil {
t.Errorf("Get(ftps) error = %v, want nil", err)
}
if p.Scheme() != "ftp" {
t.Errorf("scheme = %q, want %q", p.Scheme(), "ftp")
}
})
t.Run("http stays as http", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
if !r.Supports("http") {
t.Error("Supports(http) = false, want true")
}
})
t.Run("unknown scheme not normalized", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
if r.Supports("") {
t.Error("Supports(\"\") = true, want false")
}
if r.Supports("unknown") {
t.Error("Supports(unknown) = true, want false")
}
})
t.Run("case insensitive normalization", func(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register() error = %v", err)
}
if !r.Supports("HTTPS") {
t.Error("Supports(HTTPS) = false, want true (case-insensitive normalization)")
}
})
}
// ---------- Global Registry Tests ----------
func TestGlobalRegistry(t *testing.T) {
r := protocol.GlobalRegistry
if r == nil {
t.Fatal("GlobalRegistry is nil")
}
schemes := r.List()
if schemes == nil {
t.Error("List() on global registry returned nil")
}
}
func TestGlobalRegister(t *testing.T) {
uniqueProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "TestProto",
Scheme: "testglobal",
}),
}
err := protocol.Register(uniqueProto)
if err != nil {
t.Errorf("Register() on global registry: error = %v", err)
}
}
func TestGlobalGet(t *testing.T) {
_, err := protocol.Get(nil)
if err == nil {
t.Fatal("Get(nil) on global registry: error = nil, want error")
}
if err.Error() != "url cannot be nil" {
t.Errorf("Error() = %q, want %q", err.Error(), "url cannot be nil")
}
}
// ---------- Concurrency Tests ----------
func TestRegistryConcurrency(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
Features: []string{"resume", "compression", "range"},
}),
}
if err := r.Register(httpProto); err != nil {
t.Fatalf("Register(http) error = %v", err)
}
done := make(chan bool, 10)
for i := 0; i < 10; i++ {
go func() {
_ = r.List()
_ = r.Supports("http")
_ = r.Supports("https")
_ = r.Supports("ftp")
_ = r.Capabilities()
u, _ := url.Parse("http://example.com/file.txt")
_, _ = r.Get(u)
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
func TestRegistryRegisterReadConcurrency(t *testing.T) {
r := protocol.NewRegistry()
httpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "HTTP",
Scheme: "http",
}),
}
r.Register(httpProto)
done := make(chan bool, 2)
go func() {
ftpProto := &mockProtocol{
BaseProtocol: protocol.NewBaseProtocol(protocol.ProtocolInfo{
Name: "FTP",
Scheme: "ftp",
}),
}
r.Register(ftpProto)
done <- true
}()
go func() {
_ = r.List()
_ = r.Supports("http")
_ = r.Capabilities()
u, _ := url.Parse("http://example.com/file.txt")
_, _ = r.Get(u)
done <- true
}()
<-done
<-done
}