//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 }