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