1077 lines
29 KiB
Go
1077 lines
29 KiB
Go
//go:build linux || freebsd
|
|||
|
|
// +build linux freebsd
|
||
|
|
|
||
|
|
package core
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"net/url"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Config Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
func TestDefaultTimeoutConfig(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
|
||
|
|
if tc.Min != 30*time.Second {
|
||
|
|
t.Errorf("Min = %v, want 30s", tc.Min)
|
||
|
|
}
|
||
|
|
if tc.Max != 24*time.Hour {
|
||
|
|
t.Errorf("Max = %v, want 24h", tc.Max)
|
||
|
|
}
|
||
|
|
if tc.MinSpeedBytesPerSec != 10*1024 {
|
||
|
|
t.Errorf("MinSpeedBytesPerSec = %d, want %d", tc.MinSpeedBytesPerSec, 10*1024)
|
||
|
|
}
|
||
|
|
if tc.SafetyFactor != 3.0 {
|
||
|
|
t.Errorf("SafetyFactor = %f, want 3.0", tc.SafetyFactor)
|
||
|
|
}
|
||
|
|
if tc.DefaultUnknown != 30*time.Minute {
|
||
|
|
t.Errorf("DefaultUnknown = %v, want 30m", tc.DefaultUnknown)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCalculateTimeout_UserOverride(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
override := 5 * time.Minute
|
||
|
|
|
||
|
|
result := tc.CalculateTimeout(100*1024*1024, override)
|
||
|
|
|
||
|
|
if result != override {
|
||
|
|
t.Errorf("CalculateTimeout = %v, want %v", result, override)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCalculateTimeout_ZeroFileSize(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
|
||
|
|
result := tc.CalculateTimeout(0, 0)
|
||
|
|
|
||
|
|
if result != tc.DefaultUnknown {
|
||
|
|
t.Errorf("CalculateTimeout(0, 0) = %v, want %v", result, tc.DefaultUnknown)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCalculateTimeout_NegativeFileSize(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
|
||
|
|
result := tc.CalculateTimeout(-1, 0)
|
||
|
|
|
||
|
|
if result != tc.DefaultUnknown {
|
||
|
|
t.Errorf("CalculateTimeout(-1, 0) = %v, want %v", result, tc.DefaultUnknown)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCalculateTimeout_AutoCalculation(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
// fileSize = 1MB, minSpeed = 10240 bytes/sec (10 KB/s)
|
||
|
|
// expectedSeconds = 1048576 / 10240 = 102.4s
|
||
|
|
// calculated = 102.4 * 3.0 = 307.2s
|
||
|
|
fileSize := int64(1 * 1024 * 1024) // 1MB
|
||
|
|
|
||
|
|
result := tc.CalculateTimeout(fileSize, 0)
|
||
|
|
|
||
|
|
if result < tc.Min {
|
||
|
|
t.Errorf("CalculateTimeout(%d, 0) = %v, should be >= Min (%v)", fileSize, result, tc.Min)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCalculateTimeout_ClampedToMin(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
// tiny file: 1 byte
|
||
|
|
// expectedSeconds = 1 / 10240 ≈ 0.000097s
|
||
|
|
// calculated = 0.000097 * 3.0 ≈ 0.00029s
|
||
|
|
// clamped to Min = 30s
|
||
|
|
fileSize := int64(1)
|
||
|
|
|
||
|
|
result := tc.CalculateTimeout(fileSize, 0)
|
||
|
|
|
||
|
|
if result != tc.Min {
|
||
|
|
t.Errorf("CalculateTimeout(%d, 0) = %v, want Min (%v)", fileSize, result, tc.Min)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCalculateTimeout_ClampedToMax(t *testing.T) {
|
||
|
|
tc := DefaultTimeoutConfig()
|
||
|
|
// huge file: 10TB
|
||
|
|
// expectedSeconds = 10*1024*1024*1024*1024 / 10240 ≈ 1,099,511,627,776s
|
||
|
|
// calculated = that * 3.0 → enormous
|
||
|
|
// clamped to Max = 24h
|
||
|
|
fileSize := int64(10 * 1024 * 1024 * 1024 * 1024) // 10TB
|
||
|
|
|
||
|
|
result := tc.CalculateTimeout(fileSize, 0)
|
||
|
|
|
||
|
|
if result != tc.Max {
|
||
|
|
t.Errorf("CalculateTimeout(%d, 0) = %v, want Max (%v)", fileSize, result, tc.Max)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Context Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
func TestNewRequestContext(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
rc := NewRequestContext(ctx, "req-123")
|
||
|
|
|
||
|
|
if rc == nil {
|
||
|
|
t.Fatal("NewRequestContext returned nil")
|
||
|
|
}
|
||
|
|
if rc.RequestID != "req-123" {
|
||
|
|
t.Errorf("RequestID = %q, want %q", rc.RequestID, "req-123")
|
||
|
|
}
|
||
|
|
if rc.Context != ctx {
|
||
|
|
t.Errorf("Context mismatch")
|
||
|
|
}
|
||
|
|
if rc.IPVersion != 0 {
|
||
|
|
t.Errorf("IPVersion = %d, want 0", rc.IPVersion)
|
||
|
|
}
|
||
|
|
if rc.AttemptCount != 0 {
|
||
|
|
t.Errorf("AttemptCount = %d, want 0", rc.AttemptCount)
|
||
|
|
}
|
||
|
|
if rc.ProtocolChain == nil {
|
||
|
|
t.Errorf("ProtocolChain is nil, want initialized slice")
|
||
|
|
}
|
||
|
|
if rc.StartTime.IsZero() {
|
||
|
|
t.Errorf("StartTime is zero")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDuration(t *testing.T) {
|
||
|
|
rc := NewRequestContext(context.Background(), "req-1")
|
||
|
|
|
||
|
|
// Duration should be a small positive value
|
||
|
|
d := rc.Duration()
|
||
|
|
if d < 0 {
|
||
|
|
t.Errorf("Duration = %v, want >= 0", d)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sleep briefly to ensure duration increases
|
||
|
|
time.Sleep(2 * time.Millisecond)
|
||
|
|
d2 := rc.Duration()
|
||
|
|
if d2 <= d {
|
||
|
|
t.Errorf("Duration after sleep = %v, should be > %v", d2, d)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAddProtocol(t *testing.T) {
|
||
|
|
rc := NewRequestContext(context.Background(), "req-1")
|
||
|
|
|
||
|
|
if len(rc.ProtocolChain) != 0 {
|
||
|
|
t.Errorf("ProtocolChain length = %d, want 0", len(rc.ProtocolChain))
|
||
|
|
}
|
||
|
|
|
||
|
|
rc.AddProtocol("http")
|
||
|
|
if len(rc.ProtocolChain) != 1 {
|
||
|
|
t.Errorf("ProtocolChain length = %d, want 1", len(rc.ProtocolChain))
|
||
|
|
}
|
||
|
|
if rc.ProtocolChain[0] != "http" {
|
||
|
|
t.Errorf("ProtocolChain[0] = %q, want %q", rc.ProtocolChain[0], "http")
|
||
|
|
}
|
||
|
|
|
||
|
|
rc.AddProtocol("https")
|
||
|
|
if len(rc.ProtocolChain) != 2 {
|
||
|
|
t.Errorf("ProtocolChain length = %d, want 2", len(rc.ProtocolChain))
|
||
|
|
}
|
||
|
|
if rc.ProtocolChain[1] != "https" {
|
||
|
|
t.Errorf("ProtocolChain[1] = %q, want %q", rc.ProtocolChain[1], "https")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIsCancelled_NotCancelled(t *testing.T) {
|
||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
|
defer cancel()
|
||
|
|
rc := NewRequestContext(ctx, "req-1")
|
||
|
|
|
||
|
|
if rc.IsCancelled() {
|
||
|
|
t.Errorf("IsCancelled = true, want false")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIsCancelled_Cancelled(t *testing.T) {
|
||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
|
cancel() // cancel immediately
|
||
|
|
rc := NewRequestContext(ctx, "req-1")
|
||
|
|
|
||
|
|
if !rc.IsCancelled() {
|
||
|
|
t.Errorf("IsCancelled = false, want true")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestIsCancelled_WithDeadline(t *testing.T) {
|
||
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
|
||
|
|
defer cancel()
|
||
|
|
|
||
|
|
time.Sleep(5 * time.Millisecond)
|
||
|
|
|
||
|
|
rc := NewRequestContext(ctx, "req-1")
|
||
|
|
if !rc.IsCancelled() {
|
||
|
|
t.Errorf("IsCancelled = false, want true (deadline exceeded)")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestRequestContextFields(t *testing.T) {
|
||
|
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
|
defer cancel()
|
||
|
|
rc := NewRequestContext(ctx, "req-init")
|
||
|
|
|
||
|
|
if rc.RequestID != "req-init" {
|
||
|
|
t.Errorf("RequestID = %q, want %q", rc.RequestID, "req-init")
|
||
|
|
}
|
||
|
|
if rc.StartTime.IsZero() {
|
||
|
|
t.Errorf("StartTime is zero")
|
||
|
|
}
|
||
|
|
if rc.ConnectedIP != "" {
|
||
|
|
t.Errorf("ConnectedIP = %q, want empty string", rc.ConnectedIP)
|
||
|
|
}
|
||
|
|
if rc.IPVersion != 0 {
|
||
|
|
t.Errorf("IPVersion = %d, want 0", rc.IPVersion)
|
||
|
|
}
|
||
|
|
if rc.AttemptCount != 0 {
|
||
|
|
t.Errorf("AttemptCount = %d, want 0", rc.AttemptCount)
|
||
|
|
}
|
||
|
|
if rc.ProtocolChain == nil {
|
||
|
|
t.Errorf("ProtocolChain is nil, want empty slice")
|
||
|
|
}
|
||
|
|
if len(rc.ProtocolChain) != 0 {
|
||
|
|
t.Errorf("len(ProtocolChain) = %d, want 0", len(rc.ProtocolChain))
|
||
|
|
}
|
||
|
|
if rc.Context != ctx {
|
||
|
|
t.Errorf("Context mismatch")
|
||
|
|
}
|
||
|
|
|
||
|
|
// Mutate fields after creation
|
||
|
|
rc.ConnectedIP = "192.168.1.1"
|
||
|
|
rc.IPVersion = 4
|
||
|
|
rc.AttemptCount = 2
|
||
|
|
|
||
|
|
if rc.ConnectedIP != "192.168.1.1" {
|
||
|
|
t.Errorf("ConnectedIP after mutation = %q, want %q", rc.ConnectedIP, "192.168.1.1")
|
||
|
|
}
|
||
|
|
if rc.IPVersion != 4 {
|
||
|
|
t.Errorf("IPVersion after mutation = %d, want 4", rc.IPVersion)
|
||
|
|
}
|
||
|
|
if rc.AttemptCount != 2 {
|
||
|
|
t.Errorf("AttemptCount after mutation = %d, want 2", rc.AttemptCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewRequestContext_NilContext(t *testing.T) {
|
||
|
|
// Should not panic when given a nil context
|
||
|
|
rc := NewRequestContext(context.TODO(), "req-nil")
|
||
|
|
|
||
|
|
if rc == nil {
|
||
|
|
t.Fatal("NewRequestContext returned nil")
|
||
|
|
}
|
||
|
|
if rc.RequestID != "req-nil" {
|
||
|
|
t.Errorf("RequestID = %q, want %q", rc.RequestID, "req-nil")
|
||
|
|
}
|
||
|
|
if rc.Context != context.TODO() {
|
||
|
|
t.Errorf("Context should be nil")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Errors Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
func TestGogetError_WithoutCause(t *testing.T) {
|
||
|
|
err := &GogetError{
|
||
|
|
Type: ErrTimeout,
|
||
|
|
Message: "request timed out",
|
||
|
|
}
|
||
|
|
|
||
|
|
expected := "[TIMEOUT] request timed out"
|
||
|
|
if err.Error() != expected {
|
||
|
|
t.Errorf("Error() = %q, want %q", err.Error(), expected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGogetError_WithCause(t *testing.T) {
|
||
|
|
cause := errors.New("connection refused")
|
||
|
|
err := &GogetError{
|
||
|
|
Type: ErrNetwork,
|
||
|
|
Message: "failed to connect",
|
||
|
|
Cause: cause,
|
||
|
|
URL: "http://example.com/file",
|
||
|
|
}
|
||
|
|
|
||
|
|
expected := "[NETWORK] failed to connect: connection refused"
|
||
|
|
if err.Error() != expected {
|
||
|
|
t.Errorf("Error() = %q, want %q", err.Error(), expected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorTypes_NewNetworkError(t *testing.T) {
|
||
|
|
cause := errors.New("connection reset")
|
||
|
|
err := NewNetworkError("network failure", cause, "http://example.com/file")
|
||
|
|
|
||
|
|
if err.Type != ErrNetwork {
|
||
|
|
t.Errorf("Type = %q, want %q", err.Type, ErrNetwork)
|
||
|
|
}
|
||
|
|
if err.Message != "network failure" {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "network failure")
|
||
|
|
}
|
||
|
|
if err.Cause != cause {
|
||
|
|
t.Errorf("Cause mismatch")
|
||
|
|
}
|
||
|
|
if err.URL != "http://example.com/file" {
|
||
|
|
t.Errorf("URL = %q, want %q", err.URL, "http://example.com/file")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorTypes_NewProtocolError(t *testing.T) {
|
||
|
|
cause := errors.New("unsupported protocol version")
|
||
|
|
err := NewProtocolError("protocol error", cause, "ftp://example.com/file")
|
||
|
|
|
||
|
|
if err.Type != ErrProtocol {
|
||
|
|
t.Errorf("Type = %q, want %q", err.Type, ErrProtocol)
|
||
|
|
}
|
||
|
|
if err.Message != "protocol error" {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "protocol error")
|
||
|
|
}
|
||
|
|
if err.Cause != cause {
|
||
|
|
t.Errorf("Cause mismatch")
|
||
|
|
}
|
||
|
|
if err.URL != "ftp://example.com/file" {
|
||
|
|
t.Errorf("URL = %q, want %q", err.URL, "ftp://example.com/file")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorTypes_NewFileError(t *testing.T) {
|
||
|
|
cause := errors.New("permission denied")
|
||
|
|
err := NewFileError("cannot write file", cause)
|
||
|
|
|
||
|
|
if err.Type != ErrFile {
|
||
|
|
t.Errorf("Type = %q, want %q", err.Type, ErrFile)
|
||
|
|
}
|
||
|
|
if err.Message != "cannot write file" {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "cannot write file")
|
||
|
|
}
|
||
|
|
if err.Cause != cause {
|
||
|
|
t.Errorf("Cause mismatch")
|
||
|
|
}
|
||
|
|
if err.URL != "" {
|
||
|
|
t.Errorf("URL = %q, want empty", err.URL)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorTypes_NewTimeoutError(t *testing.T) {
|
||
|
|
err := NewTimeoutError("operation timed out", "http://slow.example.com/file")
|
||
|
|
|
||
|
|
if err.Type != ErrTimeout {
|
||
|
|
t.Errorf("Type = %q, want %q", err.Type, ErrTimeout)
|
||
|
|
}
|
||
|
|
if err.Message != "operation timed out" {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "operation timed out")
|
||
|
|
}
|
||
|
|
if err.Cause != nil {
|
||
|
|
t.Errorf("Cause = %v, want nil", err.Cause)
|
||
|
|
}
|
||
|
|
if err.URL != "http://slow.example.com/file" {
|
||
|
|
t.Errorf("URL = %q, want %q", err.URL, "http://slow.example.com/file")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorTypes_NewChecksumError(t *testing.T) {
|
||
|
|
err := NewChecksumError("abc123", "def456")
|
||
|
|
|
||
|
|
if err.Type != ErrChecksum {
|
||
|
|
t.Errorf("Type = %q, want %q", err.Type, ErrChecksum)
|
||
|
|
}
|
||
|
|
if err.Message != "checksum mismatch: expected abc123, got def456" {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "checksum mismatch: expected abc123, got def456")
|
||
|
|
}
|
||
|
|
if err.Details == nil {
|
||
|
|
t.Fatal("Details is nil")
|
||
|
|
}
|
||
|
|
if err.Details["expected"] != "abc123" {
|
||
|
|
t.Errorf("Details[expected] = %q, want %q", err.Details["expected"], "abc123")
|
||
|
|
}
|
||
|
|
if err.Details["actual"] != "def456" {
|
||
|
|
t.Errorf("Details[actual] = %q, want %q", err.Details["actual"], "def456")
|
||
|
|
}
|
||
|
|
if err.Cause != nil {
|
||
|
|
t.Errorf("Cause = %v, want nil", err.Cause)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorTypes_NewUnsupportedError(t *testing.T) {
|
||
|
|
err := NewUnsupportedError("feature not implemented")
|
||
|
|
|
||
|
|
if err.Type != ErrUnsupported {
|
||
|
|
t.Errorf("Type = %q, want %q", err.Type, ErrUnsupported)
|
||
|
|
}
|
||
|
|
if err.Message != "feature not implemented" {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "feature not implemented")
|
||
|
|
}
|
||
|
|
if err.Cause != nil {
|
||
|
|
t.Errorf("Cause = %v, want nil", err.Cause)
|
||
|
|
}
|
||
|
|
if err.URL != "" {
|
||
|
|
t.Errorf("URL = %q, want empty", err.URL)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorUnwrap(t *testing.T) {
|
||
|
|
cause := errors.New("underlying error")
|
||
|
|
err := NewNetworkError("network failure", cause, "http://example.com/file")
|
||
|
|
|
||
|
|
if !errors.Is(err, cause) {
|
||
|
|
t.Errorf("errors.Is(err, cause) = false, want true")
|
||
|
|
}
|
||
|
|
|
||
|
|
var target *GogetError
|
||
|
|
if !errors.As(err, &target) {
|
||
|
|
t.Errorf("errors.As(err, &target) = false, want true")
|
||
|
|
}
|
||
|
|
if target.Type != ErrNetwork {
|
||
|
|
t.Errorf("target.Type = %q, want %q", target.Type, ErrNetwork)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorUnwrap_NoCause(t *testing.T) {
|
||
|
|
err := NewTimeoutError("timed out", "http://example.com/file")
|
||
|
|
|
||
|
|
if err.Unwrap() != nil {
|
||
|
|
t.Errorf("Unwrap() = %v, want nil", err.Unwrap())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorString_WithCause(t *testing.T) {
|
||
|
|
cause := errors.New("connection timeout")
|
||
|
|
err := NewNetworkError("network error", cause, "http://example.com")
|
||
|
|
|
||
|
|
expected := "[NETWORK] network error: connection timeout"
|
||
|
|
if err.Error() != expected {
|
||
|
|
t.Errorf("Error() = %q, want %q", err.Error(), expected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestErrorString_WithoutCause(t *testing.T) {
|
||
|
|
err := NewUnsupportedError("unsupported feature")
|
||
|
|
|
||
|
|
expected := "[UNSUPPORTED] unsupported feature"
|
||
|
|
if err.Error() != expected {
|
||
|
|
t.Errorf("Error() = %q, want %q", err.Error(), expected)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetDetail(t *testing.T) {
|
||
|
|
err := NewChecksumError("abc", "xyz")
|
||
|
|
if got := err.GetDetail("expected"); got != "abc" {
|
||
|
|
t.Errorf("GetDetail(expected) = %q, want %q", got, "abc")
|
||
|
|
}
|
||
|
|
if got := err.GetDetail("actual"); got != "xyz" {
|
||
|
|
t.Errorf("GetDetail(actual) = %q, want %q", got, "xyz")
|
||
|
|
}
|
||
|
|
if got := err.GetDetail("missing"); got != "" {
|
||
|
|
t.Errorf("GetDetail(missing) = %q, want empty", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetDetailNoDetails(t *testing.T) {
|
||
|
|
err := NewTimeoutError("timed out", "http://example.com")
|
||
|
|
if got := err.GetDetail("anything"); got != "" {
|
||
|
|
t.Errorf("GetDetail on error without Details = %q, want empty", got)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewChecksumError_EmptyStrings(t *testing.T) {
|
||
|
|
err := NewChecksumError("", "")
|
||
|
|
|
||
|
|
if err.Message != "checksum mismatch: expected , got " {
|
||
|
|
t.Errorf("Message = %q, want %q", err.Message, "checksum mismatch: expected , got ")
|
||
|
|
}
|
||
|
|
if err.Details["expected"] != "" {
|
||
|
|
t.Errorf("Details[expected] = %q, want empty", err.Details["expected"])
|
||
|
|
}
|
||
|
|
if err.Details["actual"] != "" {
|
||
|
|
t.Errorf("Details[actual] = %q, want empty", err.Details["actual"])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewChecksumError_DetailsMap(t *testing.T) {
|
||
|
|
err := NewChecksumError("abc", "xyz")
|
||
|
|
|
||
|
|
if _, ok := err.Details["expected"]; !ok {
|
||
|
|
t.Errorf("Details missing key 'expected'")
|
||
|
|
}
|
||
|
|
if _, ok := err.Details["actual"]; !ok {
|
||
|
|
t.Errorf("Details missing key 'actual'")
|
||
|
|
}
|
||
|
|
if len(err.Details) != 2 {
|
||
|
|
t.Errorf("len(Details) = %d, want 2", len(err.Details))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Types Tests
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
func TestDefaultParallelConfig(t *testing.T) {
|
||
|
|
pc := DefaultParallelConfig()
|
||
|
|
|
||
|
|
if pc.Connections != 0 {
|
||
|
|
t.Errorf("Connections = %d, want 0", pc.Connections)
|
||
|
|
}
|
||
|
|
if pc.MinSize != 100*1024*1024 {
|
||
|
|
t.Errorf("MinSize = %d, want %d", pc.MinSize, 100*1024*1024)
|
||
|
|
}
|
||
|
|
if pc.MinChunkSize != 1*1024*1024 {
|
||
|
|
t.Errorf("MinChunkSize = %d, want %d", pc.MinChunkSize, 1*1024*1024)
|
||
|
|
}
|
||
|
|
if pc.MaxChunkSize != 50*1024*1024 {
|
||
|
|
t.Errorf("MaxChunkSize = %d, want %d", pc.MaxChunkSize, 50*1024*1024)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetConnections_Default(t *testing.T) {
|
||
|
|
pc := DefaultParallelConfig()
|
||
|
|
|
||
|
|
// connections = 0 (default), fileSize < MinSize → should return 1
|
||
|
|
result := pc.GetConnections(50 * 1024 * 1024) // 50MB
|
||
|
|
if result != 1 {
|
||
|
|
t.Errorf("GetConnections(50MB) = %d, want 1", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetConnections_ParallelEnabled(t *testing.T) {
|
||
|
|
pc := DefaultParallelConfig()
|
||
|
|
|
||
|
|
// connections = 0 (default), fileSize >= MinSize → should return 4
|
||
|
|
result := pc.GetConnections(100 * 1024 * 1024) // 100MB
|
||
|
|
if result != 4 {
|
||
|
|
t.Errorf("GetConnections(100MB) = %d, want 4", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetConnections_CustomConnectionCount(t *testing.T) {
|
||
|
|
pc := DefaultParallelConfig()
|
||
|
|
pc.Connections = 8
|
||
|
|
|
||
|
|
// connections = 8 (explicit), should return 8 regardless of size
|
||
|
|
result := pc.GetConnections(0)
|
||
|
|
if result != 8 {
|
||
|
|
t.Errorf("GetConnections(0) with Connections=8 = %d, want 8", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
result = pc.GetConnections(1 * 1024 * 1024 * 1024) // 1GB
|
||
|
|
if result != 8 {
|
||
|
|
t.Errorf("GetConnections(1GB) with Connections=8 = %d, want 8", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGetConnections_EdgeCases(t *testing.T) {
|
||
|
|
pc := DefaultParallelConfig()
|
||
|
|
|
||
|
|
// Exactly at MinSize boundary
|
||
|
|
result := pc.GetConnections(pc.MinSize)
|
||
|
|
if result != 4 {
|
||
|
|
t.Errorf("GetConnections(MinSize) = %d, want 4", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Negative file size — treated as < MinSize
|
||
|
|
result = pc.GetConnections(-1)
|
||
|
|
if result != 1 {
|
||
|
|
t.Errorf("GetConnections(-1) = %d, want 1", result)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Zero file size — treated as < MinSize
|
||
|
|
result = pc.GetConnections(0)
|
||
|
|
if result != 1 {
|
||
|
|
t.Errorf("GetConnections(0) = %d, want 1", result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDownloadRequestFields(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
u, _ := url.Parse("https://example.com/file.zip")
|
||
|
|
|
||
|
|
req := &DownloadRequest{
|
||
|
|
URL: u,
|
||
|
|
Output: "/tmp/file.zip",
|
||
|
|
Resume: true,
|
||
|
|
Timeout: 30 * time.Second,
|
||
|
|
Verbose: true,
|
||
|
|
DebugTransport: true,
|
||
|
|
Checksum: "sha256:abc123",
|
||
|
|
Headers: map[string]string{"Authorization": "Bearer token"},
|
||
|
|
Proxy: "http://proxy:8080",
|
||
|
|
AutoDecompress: true,
|
||
|
|
Recursive: true,
|
||
|
|
MaxDepth: 3,
|
||
|
|
Ctx: ctx,
|
||
|
|
ResumeInfo: &ResumeInfo{
|
||
|
|
DownloadedBytes: 1000,
|
||
|
|
ETag: `"abc123"`,
|
||
|
|
},
|
||
|
|
Parallel: DefaultParallelConfig(),
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.URL.String() != "https://example.com/file.zip" {
|
||
|
|
t.Errorf("URL = %q, want %q", req.URL.String(), "https://example.com/file.zip")
|
||
|
|
}
|
||
|
|
if req.Output != "/tmp/file.zip" {
|
||
|
|
t.Errorf("Output = %q, want %q", req.Output, "/tmp/file.zip")
|
||
|
|
}
|
||
|
|
if !req.Resume {
|
||
|
|
t.Errorf("Resume = false, want true")
|
||
|
|
}
|
||
|
|
if req.Timeout != 30*time.Second {
|
||
|
|
t.Errorf("Timeout = %v, want 30s", req.Timeout)
|
||
|
|
}
|
||
|
|
if !req.Verbose {
|
||
|
|
t.Errorf("Verbose = false, want true")
|
||
|
|
}
|
||
|
|
if !req.DebugTransport {
|
||
|
|
t.Errorf("DebugTransport = false, want true")
|
||
|
|
}
|
||
|
|
if req.Checksum != "sha256:abc123" {
|
||
|
|
t.Errorf("Checksum = %q, want %q", req.Checksum, "sha256:abc123")
|
||
|
|
}
|
||
|
|
if req.Headers["Authorization"] != "Bearer token" {
|
||
|
|
t.Errorf("Headers[Authorization] = %q, want %q", req.Headers["Authorization"], "Bearer token")
|
||
|
|
}
|
||
|
|
if req.Proxy != "http://proxy:8080" {
|
||
|
|
t.Errorf("Proxy = %q, want %q", req.Proxy, "http://proxy:8080")
|
||
|
|
}
|
||
|
|
if !req.AutoDecompress {
|
||
|
|
t.Errorf("AutoDecompress = false, want true")
|
||
|
|
}
|
||
|
|
if !req.Recursive {
|
||
|
|
t.Errorf("Recursive = false, want true")
|
||
|
|
}
|
||
|
|
if req.MaxDepth != 3 {
|
||
|
|
t.Errorf("MaxDepth = %d, want 3", req.MaxDepth)
|
||
|
|
}
|
||
|
|
if req.Ctx != ctx {
|
||
|
|
t.Errorf("Ctx mismatch")
|
||
|
|
}
|
||
|
|
if req.ResumeInfo == nil {
|
||
|
|
t.Fatal("ResumeInfo is nil")
|
||
|
|
}
|
||
|
|
if req.ResumeInfo.DownloadedBytes != 1000 {
|
||
|
|
t.Errorf("ResumeInfo.DownloadedBytes = %d, want 1000", req.ResumeInfo.DownloadedBytes)
|
||
|
|
}
|
||
|
|
if req.ResumeInfo.ETag != `"abc123"` {
|
||
|
|
t.Errorf("ResumeInfo.ETag = %q, want %q", req.ResumeInfo.ETag, `"abc123"`)
|
||
|
|
}
|
||
|
|
if req.Parallel == nil {
|
||
|
|
t.Fatal("Parallel is nil")
|
||
|
|
}
|
||
|
|
if req.Parallel.Connections != 0 {
|
||
|
|
t.Errorf("Parallel.Connections = %d, want 0", req.Parallel.Connections)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDownloadRequest_DefaultFields(t *testing.T) {
|
||
|
|
// Test zero-value initialization
|
||
|
|
req := &DownloadRequest{}
|
||
|
|
|
||
|
|
if req.URL != nil {
|
||
|
|
t.Errorf("URL = %v, want nil", req.URL)
|
||
|
|
}
|
||
|
|
if req.Output != "" {
|
||
|
|
t.Errorf("Output = %q, want empty", req.Output)
|
||
|
|
}
|
||
|
|
if req.Resume {
|
||
|
|
t.Errorf("Resume = true, want false")
|
||
|
|
}
|
||
|
|
if req.Timeout != 0 {
|
||
|
|
t.Errorf("Timeout = %v, want 0", req.Timeout)
|
||
|
|
}
|
||
|
|
if req.Verbose {
|
||
|
|
t.Errorf("Verbose = true, want false")
|
||
|
|
}
|
||
|
|
if req.MaxDepth != 0 {
|
||
|
|
t.Errorf("MaxDepth = %d, want 0", req.MaxDepth)
|
||
|
|
}
|
||
|
|
if req.Writer != nil {
|
||
|
|
t.Errorf("Writer = %v, want nil", req.Writer)
|
||
|
|
}
|
||
|
|
if req.ProgressCallback != nil {
|
||
|
|
t.Errorf("ProgressCallback = %p, want nil", req.ProgressCallback)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDownloadResultFields(t *testing.T) {
|
||
|
|
result := &DownloadResult{
|
||
|
|
BytesDownloaded: 1024,
|
||
|
|
TotalSize: 2048,
|
||
|
|
Duration: 5 * time.Second,
|
||
|
|
Protocol: "HTTPS",
|
||
|
|
IPVersion: 6,
|
||
|
|
Speed: 204.8,
|
||
|
|
OutputPath: "/tmp/file.zip",
|
||
|
|
Checksum: "sha256:abc123",
|
||
|
|
Resumed: true,
|
||
|
|
Parallel: true,
|
||
|
|
ChunksCount: 4,
|
||
|
|
}
|
||
|
|
|
||
|
|
if result.BytesDownloaded != 1024 {
|
||
|
|
t.Errorf("BytesDownloaded = %d, want 1024", result.BytesDownloaded)
|
||
|
|
}
|
||
|
|
if result.TotalSize != 2048 {
|
||
|
|
t.Errorf("TotalSize = %d, want 2048", result.TotalSize)
|
||
|
|
}
|
||
|
|
if result.Duration != 5*time.Second {
|
||
|
|
t.Errorf("Duration = %v, want 5s", result.Duration)
|
||
|
|
}
|
||
|
|
if result.Protocol != "HTTPS" {
|
||
|
|
t.Errorf("Protocol = %q, want %q", result.Protocol, "HTTPS")
|
||
|
|
}
|
||
|
|
if result.IPVersion != 6 {
|
||
|
|
t.Errorf("IPVersion = %d, want 6", result.IPVersion)
|
||
|
|
}
|
||
|
|
if result.Speed != 204.8 {
|
||
|
|
t.Errorf("Speed = %f, want 204.8", result.Speed)
|
||
|
|
}
|
||
|
|
if result.OutputPath != "/tmp/file.zip" {
|
||
|
|
t.Errorf("OutputPath = %q, want %q", result.OutputPath, "/tmp/file.zip")
|
||
|
|
}
|
||
|
|
if result.Checksum != "sha256:abc123" {
|
||
|
|
t.Errorf("Checksum = %q, want %q", result.Checksum, "sha256:abc123")
|
||
|
|
}
|
||
|
|
if !result.Resumed {
|
||
|
|
t.Errorf("Resumed = false, want true")
|
||
|
|
}
|
||
|
|
if !result.Parallel {
|
||
|
|
t.Errorf("Parallel = false, want true")
|
||
|
|
}
|
||
|
|
if result.ChunksCount != 4 {
|
||
|
|
t.Errorf("ChunksCount = %d, want 4", result.ChunksCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDownloadResult_ZeroValues(t *testing.T) {
|
||
|
|
result := &DownloadResult{}
|
||
|
|
|
||
|
|
if result.BytesDownloaded != 0 {
|
||
|
|
t.Errorf("BytesDownloaded = %d, want 0", result.BytesDownloaded)
|
||
|
|
}
|
||
|
|
if result.TotalSize != 0 {
|
||
|
|
t.Errorf("TotalSize = %d, want 0", result.TotalSize)
|
||
|
|
}
|
||
|
|
if result.Duration != 0 {
|
||
|
|
t.Errorf("Duration = %v, want 0", result.Duration)
|
||
|
|
}
|
||
|
|
if result.Protocol != "" {
|
||
|
|
t.Errorf("Protocol = %q, want empty", result.Protocol)
|
||
|
|
}
|
||
|
|
if result.IPVersion != 0 {
|
||
|
|
t.Errorf("IPVersion = %d, want 0", result.IPVersion)
|
||
|
|
}
|
||
|
|
if result.Speed != 0.0 {
|
||
|
|
t.Errorf("Speed = %f, want 0.0", result.Speed)
|
||
|
|
}
|
||
|
|
if result.OutputPath != "" {
|
||
|
|
t.Errorf("OutputPath = %q, want empty", result.OutputPath)
|
||
|
|
}
|
||
|
|
if result.Checksum != "" {
|
||
|
|
t.Errorf("Checksum = %q, want empty", result.Checksum)
|
||
|
|
}
|
||
|
|
if result.Resumed {
|
||
|
|
t.Errorf("Resumed = true, want false")
|
||
|
|
}
|
||
|
|
if result.Parallel {
|
||
|
|
t.Errorf("Parallel = true, want false")
|
||
|
|
}
|
||
|
|
if result.ChunksCount != 0 {
|
||
|
|
t.Errorf("ChunksCount = %d, want 0", result.ChunksCount)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestChunkInfo(t *testing.T) {
|
||
|
|
chunk := &ChunkInfo{
|
||
|
|
ID: 1,
|
||
|
|
Start: 0,
|
||
|
|
End: 1023,
|
||
|
|
Downloaded: 512,
|
||
|
|
Status: 200,
|
||
|
|
Error: nil,
|
||
|
|
}
|
||
|
|
|
||
|
|
if chunk.ID != 1 {
|
||
|
|
t.Errorf("ID = %d, want 1", chunk.ID)
|
||
|
|
}
|
||
|
|
if chunk.Start != 0 {
|
||
|
|
t.Errorf("Start = %d, want 0", chunk.Start)
|
||
|
|
}
|
||
|
|
if chunk.End != 1023 {
|
||
|
|
t.Errorf("End = %d, want 1023", chunk.End)
|
||
|
|
}
|
||
|
|
if chunk.Downloaded != 512 {
|
||
|
|
t.Errorf("Downloaded = %d, want 512", chunk.Downloaded)
|
||
|
|
}
|
||
|
|
if chunk.Status != 200 {
|
||
|
|
t.Errorf("Status = %d, want 200", chunk.Status)
|
||
|
|
}
|
||
|
|
if chunk.Error != nil {
|
||
|
|
t.Errorf("Error = %v, want nil", chunk.Error)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestChunkInfo_WithError(t *testing.T) {
|
||
|
|
chunkErr := errors.New("chunk download failed")
|
||
|
|
chunk := &ChunkInfo{
|
||
|
|
ID: 2,
|
||
|
|
Start: 1024,
|
||
|
|
End: 2047,
|
||
|
|
Status: 500,
|
||
|
|
Error: chunkErr,
|
||
|
|
}
|
||
|
|
|
||
|
|
if chunk.ID != 2 {
|
||
|
|
t.Errorf("ID = %d, want 2", chunk.ID)
|
||
|
|
}
|
||
|
|
if chunk.Start != 1024 {
|
||
|
|
t.Errorf("Start = %d, want 1024", chunk.Start)
|
||
|
|
}
|
||
|
|
if chunk.End != 2047 {
|
||
|
|
t.Errorf("End = %d, want 2047", chunk.End)
|
||
|
|
}
|
||
|
|
if chunk.Status != 500 {
|
||
|
|
t.Errorf("Status = %d, want 500", chunk.Status)
|
||
|
|
}
|
||
|
|
if chunk.Error != chunkErr {
|
||
|
|
t.Errorf("Error mismatch")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestResumeInfo(t *testing.T) {
|
||
|
|
now := time.Now()
|
||
|
|
ri := &ResumeInfo{
|
||
|
|
DownloadedBytes: 5000,
|
||
|
|
ETag: `"etag-value"`,
|
||
|
|
LastModified: "Mon, 02 Jan 2006 15:04:05 GMT",
|
||
|
|
URL: "https://example.com/file.part",
|
||
|
|
LastWrite: now,
|
||
|
|
Chunks: map[int]int64{
|
||
|
|
1: 1000,
|
||
|
|
2: 2000,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
if ri.DownloadedBytes != 5000 {
|
||
|
|
t.Errorf("DownloadedBytes = %d, want 5000", ri.DownloadedBytes)
|
||
|
|
}
|
||
|
|
if ri.ETag != `"etag-value"` {
|
||
|
|
t.Errorf("ETag = %q, want %q", ri.ETag, `"etag-value"`)
|
||
|
|
}
|
||
|
|
if ri.LastModified != "Mon, 02 Jan 2006 15:04:05 GMT" {
|
||
|
|
t.Errorf("LastModified = %q, want %q", ri.LastModified, "Mon, 02 Jan 2006 15:04:05 GMT")
|
||
|
|
}
|
||
|
|
if ri.URL != "https://example.com/file.part" {
|
||
|
|
t.Errorf("URL = %q, want %q", ri.URL, "https://example.com/file.part")
|
||
|
|
}
|
||
|
|
if !ri.LastWrite.Equal(now) {
|
||
|
|
t.Errorf("LastWrite = %v, want %v", ri.LastWrite, now)
|
||
|
|
}
|
||
|
|
if len(ri.Chunks) != 2 {
|
||
|
|
t.Errorf("len(Chunks) = %d, want 2", len(ri.Chunks))
|
||
|
|
}
|
||
|
|
if ri.Chunks[1] != 1000 {
|
||
|
|
t.Errorf("Chunks[1] = %d, want 1000", ri.Chunks[1])
|
||
|
|
}
|
||
|
|
if ri.Chunks[2] != 2000 {
|
||
|
|
t.Errorf("Chunks[2] = %d, want 2000", ri.Chunks[2])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProgressCallbackType(t *testing.T) {
|
||
|
|
// Test that the function signature compiles and works
|
||
|
|
var cb ProgressCallback = func(current, total int64, speed float64) {
|
||
|
|
if current < 0 {
|
||
|
|
t.Errorf("current = %d, want >= 0", current)
|
||
|
|
}
|
||
|
|
if total < 0 {
|
||
|
|
t.Errorf("total = %d, want >= 0", total)
|
||
|
|
}
|
||
|
|
if speed < 0 {
|
||
|
|
t.Errorf("speed = %f, want >= 0", speed)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ensure the callback doesn't panic
|
||
|
|
cb(100, 1000, 50.5)
|
||
|
|
cb(0, 0, 0.0)
|
||
|
|
cb(1000, 1000, 0.0)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUploadRequestFields(t *testing.T) {
|
||
|
|
ctx := context.Background()
|
||
|
|
u, _ := url.Parse("https://example.com/upload")
|
||
|
|
|
||
|
|
req := &UploadRequest{
|
||
|
|
URL: u,
|
||
|
|
Input: "/tmp/file.txt",
|
||
|
|
Timeout: 60 * time.Second,
|
||
|
|
Verbose: true,
|
||
|
|
Headers: map[string]string{"Content-Type": "text/plain"},
|
||
|
|
Proxy: "http://proxy:8080",
|
||
|
|
Ctx: ctx,
|
||
|
|
}
|
||
|
|
|
||
|
|
if req.URL.String() != "https://example.com/upload" {
|
||
|
|
t.Errorf("URL = %q, want %q", req.URL.String(), "https://example.com/upload")
|
||
|
|
}
|
||
|
|
if req.Input != "/tmp/file.txt" {
|
||
|
|
t.Errorf("Input = %q, want %q", req.Input, "/tmp/file.txt")
|
||
|
|
}
|
||
|
|
if req.Timeout != 60*time.Second {
|
||
|
|
t.Errorf("Timeout = %v, want 60s", req.Timeout)
|
||
|
|
}
|
||
|
|
if !req.Verbose {
|
||
|
|
t.Errorf("Verbose = false, want true")
|
||
|
|
}
|
||
|
|
if req.Headers["Content-Type"] != "text/plain" {
|
||
|
|
t.Errorf("Headers[Content-Type] = %q, want %q", req.Headers["Content-Type"], "text/plain")
|
||
|
|
}
|
||
|
|
if req.Proxy != "http://proxy:8080" {
|
||
|
|
t.Errorf("Proxy = %q, want %q", req.Proxy, "http://proxy:8080")
|
||
|
|
}
|
||
|
|
if req.Ctx != ctx {
|
||
|
|
t.Errorf("Ctx mismatch")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUploadResultFields(t *testing.T) {
|
||
|
|
result := &UploadResult{
|
||
|
|
BytesUploaded: 2048,
|
||
|
|
Duration: 10 * time.Second,
|
||
|
|
Protocol: "HTTPS",
|
||
|
|
ResultURL: "https://example.com/uploaded/file.txt",
|
||
|
|
}
|
||
|
|
|
||
|
|
if result.BytesUploaded != 2048 {
|
||
|
|
t.Errorf("BytesUploaded = %d, want 2048", result.BytesUploaded)
|
||
|
|
}
|
||
|
|
if result.Duration != 10*time.Second {
|
||
|
|
t.Errorf("Duration = %v, want 10s", result.Duration)
|
||
|
|
}
|
||
|
|
if result.Protocol != "HTTPS" {
|
||
|
|
t.Errorf("Protocol = %q, want %q", result.Protocol, "HTTPS")
|
||
|
|
}
|
||
|
|
if result.ResultURL != "https://example.com/uploaded/file.txt" {
|
||
|
|
t.Errorf("ResultURL = %q, want %q", result.ResultURL, "https://example.com/uploaded/file.txt")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUploadResult_ZeroValues(t *testing.T) {
|
||
|
|
result := &UploadResult{}
|
||
|
|
|
||
|
|
if result.BytesUploaded != 0 {
|
||
|
|
t.Errorf("BytesUploaded = %d, want 0", result.BytesUploaded)
|
||
|
|
}
|
||
|
|
if result.Duration != 0 {
|
||
|
|
t.Errorf("Duration = %v, want 0", result.Duration)
|
||
|
|
}
|
||
|
|
if result.Protocol != "" {
|
||
|
|
t.Errorf("Protocol = %q, want empty", result.Protocol)
|
||
|
|
}
|
||
|
|
if result.ResultURL != "" {
|
||
|
|
t.Errorf("ResultURL = %q, want empty", result.ResultURL)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Interface compile-time checks — these verify the interfaces are satisfied
|
||
|
|
var _ Protocol = (*mockProtocol)(nil)
|
||
|
|
|
||
|
|
type mockProtocol struct{}
|
||
|
|
|
||
|
|
func (m *mockProtocol) Scheme() string { return "test" }
|
||
|
|
func (m *mockProtocol) CanHandle(u *url.URL) bool { return u.Scheme == "test" }
|
||
|
|
func (m *mockProtocol) Download(ctx context.Context, req *DownloadRequest) (*DownloadResult, error) {
|
||
|
|
return &DownloadResult{}, nil
|
||
|
|
}
|
||
|
|
func (m *mockProtocol) Capabilities() []string { return nil }
|
||
|
|
func (m *mockProtocol) SupportsResume() bool { return false }
|
||
|
|
func (m *mockProtocol) SupportsCompression() bool { return false }
|
||
|
|
func (m *mockProtocol) SupportsParallel() bool { return false }
|
||
|
|
func (m *mockProtocol) SupportsRecursive() bool { return false }
|
||
|
|
|
||
|
|
var _ Uploader = (*mockUploader)(nil)
|
||
|
|
|
||
|
|
type mockUploader struct{}
|
||
|
|
|
||
|
|
func (m *mockUploader) Upload(ctx context.Context, req *UploadRequest) (*UploadResult, error) {
|
||
|
|
return &UploadResult{}, nil
|
||
|
|
}
|
||
|
|
func (m *mockUploader) SupportsUpload() bool { return true }
|
||
|
|
|
||
|
|
func TestProtocolInterface(t *testing.T) {
|
||
|
|
p := &mockProtocol{}
|
||
|
|
|
||
|
|
if p.Scheme() != "test" {
|
||
|
|
t.Errorf("Scheme() = %q, want %q", p.Scheme(), "test")
|
||
|
|
}
|
||
|
|
if !p.CanHandle(&url.URL{Scheme: "test"}) {
|
||
|
|
t.Errorf("CanHandle('test') = false, want true")
|
||
|
|
}
|
||
|
|
if p.CanHandle(&url.URL{Scheme: "http"}) {
|
||
|
|
t.Errorf("CanHandle('http') = true, want false")
|
||
|
|
}
|
||
|
|
if p.SupportsResume() {
|
||
|
|
t.Errorf("SupportsResume() = true, want false")
|
||
|
|
}
|
||
|
|
if p.SupportsCompression() {
|
||
|
|
t.Errorf("SupportsCompression() = true, want false")
|
||
|
|
}
|
||
|
|
if p.SupportsParallel() {
|
||
|
|
t.Errorf("SupportsParallel() = true, want false")
|
||
|
|
}
|
||
|
|
if p.SupportsRecursive() {
|
||
|
|
t.Errorf("SupportsRecursive() = true, want false")
|
||
|
|
}
|
||
|
|
|
||
|
|
result, err := p.Download(context.Background(), &DownloadRequest{})
|
||
|
|
if err != nil {
|
||
|
|
t.Errorf("Download() error = %v", err)
|
||
|
|
}
|
||
|
|
if result == nil {
|
||
|
|
t.Errorf("Download() returned nil result")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestUploaderInterface(t *testing.T) {
|
||
|
|
u := &mockUploader{}
|
||
|
|
|
||
|
|
if !u.SupportsUpload() {
|
||
|
|
t.Errorf("SupportsUpload() = false, want true")
|
||
|
|
}
|
||
|
|
|
||
|
|
result, err := u.Upload(context.Background(), &UploadRequest{})
|
||
|
|
if err != nil {
|
||
|
|
t.Errorf("Upload() error = %v", err)
|
||
|
|
}
|
||
|
|
if result == nil {
|
||
|
|
t.Errorf("Upload() returned nil result")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestProgressWriterInterface(t *testing.T) {
|
||
|
|
// Verify *mockProgressWriter satisfies ProgressWriter
|
||
|
|
var _ ProgressWriter = (*mockProgressWriter)(nil)
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
type mockProgressWriter struct {
|
||
|
|
written int
|
||
|
|
cb ProgressCallback
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockProgressWriter) Write(p []byte) (int, error) {
|
||
|
|
m.written += len(p)
|
||
|
|
return len(p), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *mockProgressWriter) SetProgressCallback(cb ProgressCallback) {
|
||
|
|
m.cb = cb
|
||
|
|
}
|