Files
goget/internal/core/core_test.go
T

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
}