feat: initial goget release — modern IPv6-first download utility
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
//go:build linux || freebsd
|
||||
// +build linux freebsd
|
||||
|
||||
package ftp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ---------- Protocol tests ----------
|
||||
|
||||
func TestNewProtocol(t *testing.T) {
|
||||
p := NewProtocol()
|
||||
if p == nil {
|
||||
t.Fatal("NewProtocol() returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScheme(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
if got := p.Scheme(); got != "ftp" {
|
||||
t.Errorf("Scheme() = %q, want %q", got, "ftp")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanHandle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
want bool
|
||||
}{
|
||||
{"ftp scheme", "ftp://ftp.example.com/file.txt", true},
|
||||
{"ftps scheme", "ftps://ftp.example.com/file.txt", true},
|
||||
{"http scheme", "http://example.com/file.txt", false},
|
||||
{"https scheme", "https://example.com/file.txt", false},
|
||||
{"sftp scheme", "sftp://example.com/file.txt", false},
|
||||
{"empty scheme", "file.txt", false},
|
||||
{"nil URL", "", false},
|
||||
}
|
||||
|
||||
p := &Protocol{}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var u *url.URL
|
||||
if tt.raw != "" {
|
||||
var err error
|
||||
u, err = url.Parse(tt.raw)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse URL %q: %v", tt.raw, err)
|
||||
}
|
||||
}
|
||||
if got := p.CanHandle(u); got != tt.want {
|
||||
t.Errorf("CanHandle(%q) = %v, want %v", tt.raw, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanHandleCaseInsensitive(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
u, _ := url.Parse("FTP://example.com/file.txt")
|
||||
if !p.CanHandle(u) {
|
||||
t.Errorf("CanHandle should be case-insensitive for FTP")
|
||||
}
|
||||
u, _ = url.Parse("FTPS://example.com/file.txt")
|
||||
if !p.CanHandle(u) {
|
||||
t.Errorf("CanHandle should be case-insensitive for FTPS")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilities(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
caps := p.Capabilities()
|
||||
if caps == nil {
|
||||
t.Fatal("Capabilities() returned nil")
|
||||
}
|
||||
|
||||
expected := map[string]bool{
|
||||
"download": true,
|
||||
"resume": true,
|
||||
"recursive": true,
|
||||
"tls": true,
|
||||
}
|
||||
|
||||
if len(caps) != len(expected) {
|
||||
t.Errorf("Capabilities() length = %d, want %d", len(caps), len(expected))
|
||||
}
|
||||
|
||||
for _, cap := range caps {
|
||||
if !expected[cap] {
|
||||
t.Errorf("unexpected capability: %q", cap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsResume(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
if !p.SupportsResume() {
|
||||
t.Errorf("SupportsResume() should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsCompression(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
if p.SupportsCompression() {
|
||||
t.Errorf("SupportsCompression() should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsParallel(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
if p.SupportsParallel() {
|
||||
t.Errorf("SupportsParallel() should return false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupportsRecursive(t *testing.T) {
|
||||
p := &Protocol{}
|
||||
if !p.SupportsRecursive() {
|
||||
t.Errorf("SupportsRecursive() should return true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAll(t *testing.T) {
|
||||
t.Run("normal content", func(t *testing.T) {
|
||||
input := "Hello, FTP world!"
|
||||
r := strings.NewReader(input)
|
||||
data, err := readAll(r)
|
||||
if err != nil {
|
||||
t.Fatalf("readAll() returned error: %v", err)
|
||||
}
|
||||
if string(data) != input {
|
||||
t.Errorf("readAll() = %q, want %q", string(data), input)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty reader", func(t *testing.T) {
|
||||
r := strings.NewReader("")
|
||||
data, err := readAll(r)
|
||||
if err != nil {
|
||||
t.Fatalf("readAll() returned error: %v", err)
|
||||
}
|
||||
if len(data) != 0 {
|
||||
t.Errorf("readAll() returned %d bytes, want 0", len(data))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("large content", func(t *testing.T) {
|
||||
// Create content larger than the 32KB buffer to ensure multiple reads
|
||||
content := strings.Repeat("A", 100*1024)
|
||||
r := strings.NewReader(content)
|
||||
data, err := readAll(r)
|
||||
if err != nil {
|
||||
t.Fatalf("readAll() returned error: %v", err)
|
||||
}
|
||||
if len(data) != len(content) {
|
||||
t.Errorf("readAll() returned %d bytes, want %d", len(data), len(content))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reader error", func(t *testing.T) {
|
||||
expectedErr := errors.New("read error")
|
||||
r := &errorReader{err: expectedErr}
|
||||
_, err := readAll(r)
|
||||
if err == nil {
|
||||
t.Fatal("readAll() should return error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), expectedErr.Error()) {
|
||||
t.Errorf("readAll() error = %v, want %v", err, expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// errorReader implements io.Reader that always returns an error
|
||||
type errorReader struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
// ---------- Client tests ----------
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rawURL string
|
||||
wantErr bool
|
||||
}{
|
||||
{"basic FTP URL", "ftp://ftp.example.com/pub/file.txt", false},
|
||||
{"FTP URL with path", "ftp://ftp.example.com/pub/", false},
|
||||
{"FTP URL root", "ftp://ftp.example.com", false},
|
||||
{"FTP URL with trailing slash", "ftp://ftp.example.com/", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
client, err := NewClient(tt.rawURL, 30*time.Second)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("NewClient(%q) expected error", tt.rawURL)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient(%q) returned error: %v", tt.rawURL, err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Fatal("NewClient() returned nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientWithAuth(t *testing.T) {
|
||||
client, err := NewClient("ftp://user:password@ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.user != "user" {
|
||||
t.Errorf("client.user = %q, want %q", client.user, "user")
|
||||
}
|
||||
if client.password != "password" {
|
||||
t.Errorf("client.password = %q, want %q", client.password, "password")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientWithUserOnly(t *testing.T) {
|
||||
client, err := NewClient("ftp://user@ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.user != "user" {
|
||||
t.Errorf("client.user = %q, want %q", client.user, "user")
|
||||
}
|
||||
if client.password != "anonymous@" {
|
||||
t.Errorf("client.password should default to %q when only username is provided, got %q", "anonymous@", client.password)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientFTPS(t *testing.T) {
|
||||
client, err := NewClient("ftps://ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if !client.useTLS {
|
||||
t.Errorf("client.useTLS should be true for ftps:// scheme")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientFTPSWithAuth(t *testing.T) {
|
||||
client, err := NewClient("ftps://alice:secret@ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if !client.useTLS {
|
||||
t.Errorf("client.useTLS should be true for ftps:// scheme")
|
||||
}
|
||||
if client.user != "alice" {
|
||||
t.Errorf("client.user = %q, want %q", client.user, "alice")
|
||||
}
|
||||
if client.password != "secret" {
|
||||
t.Errorf("client.password = %q, want %q", client.password, "secret")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientInvalidURL(t *testing.T) {
|
||||
// Only inputs where url.Parse itself fails
|
||||
_, err := NewClient("://invalid", 30*time.Second)
|
||||
if err == nil {
|
||||
t.Errorf("NewClient(%q) expected error", "://invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientEmptyURL(t *testing.T) {
|
||||
client, err := NewClient("", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient(\"\") returned error: %v", err)
|
||||
}
|
||||
// url.Parse("") returns an empty URL struct, so host will be empty
|
||||
if client.host != "" {
|
||||
t.Errorf("client.host = %q, want empty", client.host)
|
||||
}
|
||||
if client.port != 21 {
|
||||
t.Errorf("client.port = %d, want %d", client.port, 21)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientNoScheme(t *testing.T) {
|
||||
client, err := NewClient("not-a-url", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient(\"not-a-url\") returned error: %v", err)
|
||||
}
|
||||
// url.Parse("not-a-url") parses it as a path with empty host
|
||||
if client.host != "" {
|
||||
t.Errorf("client.host = %q, want empty", client.host)
|
||||
}
|
||||
if client.port != 21 {
|
||||
t.Errorf("client.port = %d, want %d", client.port, 21)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientDefaultPort(t *testing.T) {
|
||||
client, err := NewClient("ftp://ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.port != 21 {
|
||||
t.Errorf("client.port = %d, want %d", client.port, 21)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientCustomPort(t *testing.T) {
|
||||
client, err := NewClient("ftp://ftp.example.com:2121/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.port != 2121 {
|
||||
t.Errorf("client.port = %d, want %d", client.port, 2121)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientInvalidPort(t *testing.T) {
|
||||
_, err := NewClient("ftp://ftp.example.com:invalid/file.txt", 30*time.Second)
|
||||
if err == nil {
|
||||
t.Errorf("NewClient() expected error for invalid port")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientDefaultCredentials(t *testing.T) {
|
||||
client, err := NewClient("ftp://ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.user != "anonymous" {
|
||||
t.Errorf("default user = %q, want %q", client.user, "anonymous")
|
||||
}
|
||||
if client.password != "anonymous@" {
|
||||
t.Errorf("default password = %q, want %q", client.password, "anonymous@")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientHostParsing(t *testing.T) {
|
||||
client, err := NewClient("ftp://ftp.example.com/path/to/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.host != "ftp.example.com" {
|
||||
t.Errorf("client.host = %q, want %q", client.host, "ftp.example.com")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientPassiveModeDefault(t *testing.T) {
|
||||
client, err := NewClient("ftp://ftp.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if !client.passive {
|
||||
t.Errorf("client.passive should default to true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientTLSConfig(t *testing.T) {
|
||||
client, err := NewClient("ftps://secure.example.com/file.txt", 30*time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient() returned error: %v", err)
|
||||
}
|
||||
if client.tlsConfig == nil {
|
||||
t.Fatal("client.tlsConfig should not be nil")
|
||||
}
|
||||
if client.tlsConfig.ServerName != "secure.example.com" {
|
||||
t.Errorf("tlsConfig.ServerName = %q, want %q", client.tlsConfig.ServerName, "secure.example.com")
|
||||
}
|
||||
if client.tlsConfig.InsecureSkipVerify {
|
||||
t.Errorf("tlsConfig.InsecureSkipVerify should be false by default")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user