main.TestPartCalculate   F
last analyzed

Complexity

Conditions 14

Size

Total Lines 81
Code Lines 51

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 14
eloc 51
nop 1
dl 0
loc 81
rs 3.6
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like main.TestPartCalculate often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
package main
2
3
import (
4
	"context"
5
	"net/http"
6
	"net/http/httptest"
7
	"os"
8
	"os/user"
9
	"path/filepath"
10
	"strings"
11
	"testing"
12
	"time"
13
14
	"golang.org/x/time/rate"
15
)
16
17
func TestPartCalculate(t *testing.T) {
18
	// Disable progress bar for tests
19
	displayProgress = false
20
21
	// Setup test environment
22
	originalDataFolder := dataFolder
23
	dataFolder = ".hget_test/"
24
	defer func() {
25
		dataFolder = originalDataFolder
26
		usr, _ := user.Current()
27
		testFolder := filepath.Join(usr.HomeDir, dataFolder)
28
		os.RemoveAll(testFolder)
29
	}()
30
31
	// Test with different numbers of parts
32
	testCases := []struct {
33
		parts       int64
34
		totalSize   int64
35
		url         string
36
		expectParts int
37
	}{
38
		{10, 100, "http://foo.bar/file", 10},
39
		{5, 1000, "http://example.com/largefile", 5},
40
		{1, 50, "http://test.org/smallfile", 1},
41
		{3, 10, "http://tiny.file/data", 3}, // Small file, multiple parts
42
	}
43
44
	for _, tc := range testCases {
45
		parts := partCalculate(tc.parts, tc.totalSize, tc.url)
46
47
		// Check number of parts
48
		if len(parts) != tc.expectParts {
49
			t.Errorf("Expected %d parts, got %d", tc.expectParts, len(parts))
50
		}
51
52
		// Check part URLs
53
		for i, part := range parts {
54
			if part.URL != tc.url {
55
				t.Errorf("Part %d: Expected URL %s, got %s", i, tc.url, part.URL)
56
			}
57
58
			// Check part index
59
			if part.Index != int64(i) {
60
				t.Errorf("Part %d: Expected Index %d, got %d", i, i, part.Index)
61
			}
62
63
			// Check ranges
64
			expectedSize := tc.totalSize / tc.parts
65
			if i < int(tc.parts-1) {
66
				if part.RangeFrom != expectedSize*int64(i) {
67
					t.Errorf("Part %d: Expected RangeFrom %d, got %d",
68
						i, expectedSize*int64(i), part.RangeFrom)
69
				}
70
				if part.RangeTo != expectedSize*int64(i+1)-1 {
71
					t.Errorf("Part %d: Expected RangeTo %d, got %d",
72
						i, expectedSize*int64(i+1)-1, part.RangeTo)
73
				}
74
			} else {
75
				// Last part might be larger due to division remainder
76
				if part.RangeFrom != expectedSize*int64(i) {
77
					t.Errorf("Part %d: Expected RangeFrom %d, got %d",
78
						i, expectedSize*int64(i), part.RangeFrom)
79
				}
80
				if part.RangeTo != tc.totalSize {
81
					t.Errorf("Part %d: Expected RangeTo %d, got %d",
82
						i, tc.totalSize, part.RangeTo)
83
				}
84
			}
85
86
			// Check path format
87
			usr, _ := user.Current()
88
			expectedBasePath := filepath.Join(usr.HomeDir, dataFolder)
89
			if !strings.Contains(part.Path, expectedBasePath) {
90
				t.Errorf("Part %d: Path does not contain expected base path: %s", i, part.Path)
91
			}
92
93
			fileName := filepath.Base(part.Path)
94
			expectedPrefix := TaskFromURL(tc.url) + ".part"
95
			if !strings.HasPrefix(fileName, expectedPrefix) {
96
				t.Errorf("Part %d: Expected filename prefix %s, got %s",
97
					i, expectedPrefix, fileName)
98
			}
99
		}
100
	}
101
}
102
103
func TestProxyAwareHTTPClient(t *testing.T) {
104
	// Test with no proxy
105
	client := ProxyAwareHTTPClient("", false)
106
	if client == nil {
107
		t.Fatal("ProxyAwareHTTPClient returned nil with no proxy")
108
	}
109
110
	// Cannot easily test with an actual proxy, but can verify it doesn't crash
111
	httpProxyClient := ProxyAwareHTTPClient("http://localhost:8080", false)
112
	if httpProxyClient == nil {
113
		t.Fatal("ProxyAwareHTTPClient returned nil with HTTP proxy")
114
	}
115
116
	socksProxyClient := ProxyAwareHTTPClient("localhost:1080", false)
117
	if socksProxyClient == nil {
118
		t.Fatal("ProxyAwareHTTPClient returned nil with SOCKS proxy")
119
	}
120
121
	// Test TLS skipVerify parameter
122
	tlsClient := ProxyAwareHTTPClient("", true)
123
	if tlsClient == nil {
124
		t.Fatal("ProxyAwareHTTPClient returned nil with TLS skip verification")
125
	}
126
127
	// Can't directly access TLS config, but it shouldn't crash
128
}
129
130
// Helper function to parse integers
131
func parseInt(s string) int {
132
	n := 0
133
	for _, c := range s {
134
		n = n*10 + int(c-'0')
135
	}
136
	return n
137
}
138
139
func TestHandleCompletedPart(t *testing.T) {
140
	// Disable progress bar for tests
141
	displayProgress = false
142
143
	// Create a part that's already complete
144
	part := Part{
145
		Index:     0,
146
		URL:       "http://example.com/test",
147
		Path:      "test.part000000",
148
		RangeFrom: 100, // RangeFrom equals RangeTo means no data to download
149
		RangeTo:   100,
150
	}
151
152
	// Create channels
153
	fileChan := make(chan string, 1)
154
	stateSaveChan := make(chan Part, 1)
155
156
	// Create downloader
157
	downloader := &HTTPDownloader{
158
		url:       "http://example.com/test",
159
		file:      "test",
160
		par:       1,
161
		len:       100,
162
		parts:     []Part{part},
163
		resumable: true,
164
	}
165
166
	// Handle the completed part
167
	downloader.handleCompletedPart(part, fileChan, stateSaveChan)
168
169
	// We no longer send file paths via fileChan in handleCompletedPart
170
	select {
171
	case <-fileChan:
172
		t.Errorf("Did not expect path to be sent to fileChan")
173
	default:
174
		// ok
175
	}
176
177
	// Verify the part was sent to stateSaveChan
178
	select {
179
	case savedPart := <-stateSaveChan:
180
		if savedPart.Index != part.Index ||
181
			savedPart.URL != part.URL ||
182
			savedPart.Path != part.Path ||
183
			savedPart.RangeFrom != part.RangeFrom ||
184
			savedPart.RangeTo != part.RangeTo {
185
			t.Errorf("Saved part does not match original part")
186
		}
187
	default:
188
		t.Errorf("No part sent to stateSaveChan")
189
	}
190
}
191
192
func TestBuildRequestForPart(t *testing.T) {
193
	// Test cases for different range situations
194
	testCases := []struct {
195
		description string
196
		part        Part
197
		contentLen  int64
198
		parallelism int64
199
		expected    string
200
	}{
201
		{
202
			description: "Single connection download (no range)",
203
			part: Part{
204
				Index:     0,
205
				URL:       "http://example.com/file",
206
				Path:      "file.part000000",
207
				RangeFrom: 0,
208
				RangeTo:   100,
209
			},
210
			contentLen:  100,
211
			parallelism: 1,
212
			expected:    "", // No range header expected
213
		},
214
		{
215
			description: "Multiple connection download with middle part",
216
			part: Part{
217
				Index:     1,
218
				URL:       "http://example.com/file",
219
				Path:      "file.part000001",
220
				RangeFrom: 50,
221
				RangeTo:   99,
222
			},
223
			contentLen:  200,
224
			parallelism: 3,
225
			expected:    "bytes=50-99",
226
		},
227
		{
228
			description: "Multiple connection download with last part",
229
			part: Part{
230
				Index:     2,
231
				URL:       "http://example.com/file",
232
				Path:      "file.part000002",
233
				RangeFrom: 100,
234
				RangeTo:   200,
235
			},
236
			contentLen:  200,
237
			parallelism: 3,
238
			expected:    "bytes=100-",
239
		},
240
	}
241
242
	for _, tc := range testCases {
243
		t.Run(tc.description, func(t *testing.T) {
244
			// Create downloader
245
			downloader := &HTTPDownloader{
246
				url:       tc.part.URL,
247
				file:      "file",
248
				par:       tc.parallelism,
249
				len:       tc.contentLen,
250
				parts:     []Part{tc.part},
251
				resumable: true,
252
			}
253
254
			// Build request
255
			req, err := downloader.buildRequestForPart(context.Background(), tc.part)
256
			if err != nil {
257
				t.Fatalf("buildRequestForPart failed: %v", err)
258
			}
259
260
			// Check range header
261
			rangeHeader := req.Header.Get("Range")
262
			if tc.expected == "" {
263
				if rangeHeader != "" {
264
					t.Errorf("Expected no Range header, got '%s'", rangeHeader)
265
				}
266
			} else {
267
				if rangeHeader != tc.expected {
268
					t.Errorf("Expected Range header '%s', got '%s'", tc.expected, rangeHeader)
269
				}
270
			}
271
272
			// Check URL
273
			if req.URL.String() != tc.part.URL {
274
				t.Errorf("Expected URL %s, got %s", tc.part.URL, req.URL.String())
275
			}
276
		})
277
	}
278
}
279
280
func TestCopyContent(t *testing.T) {
281
	// Create test data
282
	testData := "This is test data for copy content"
283
284
	// Test regular copy (no rate limit)
285
	t.Run("No Rate Limit", func(t *testing.T) {
286
		// Create source and destination
287
		src := strings.NewReader(testData)
288
		var dst strings.Builder
289
290
		// Create downloader with no rate limit
291
		downloader := &HTTPDownloader{
292
			rate: 0,
293
		}
294
295
		// Copy content
296
		done := make(chan bool)
297
		go downloader.copyContent(src, &dst, done)
298
299
		// Wait for completion
300
		<-done
301
302
		// Verify copied content
303
		if dst.String() != testData {
304
			t.Errorf("Expected content '%s', got '%s'", testData, dst.String())
305
		}
306
	})
307
308
	// Test copy with rate limit (can only test that it doesn't crash)
309
	t.Run("With Rate Limit", func(t *testing.T) {
310
		// Create source and destination
311
		src := strings.NewReader(testData)
312
		var dst strings.Builder
313
314
		// Create downloader with rate limit
315
		downloader := &HTTPDownloader{
316
			rate: 1024, // 1KB/s
317
		}
318
319
		// Copy content
320
		done := make(chan bool)
321
		go downloader.copyContent(src, &dst, done)
322
323
		// Wait for completion with timeout
324
		select {
325
		case <-done:
326
			// Success
327
		case <-time.After(2 * time.Second):
328
			t.Fatalf("Copy with rate limit timed out")
329
		}
330
331
		// Verify copied content
332
		if dst.String() != testData {
333
			t.Errorf("Expected content '%s', got '%s'", testData, dst.String())
334
		}
335
	})
336
}
337
338
func TestCopyContentWithSharedLimiter(t *testing.T) {
339
	// Verify copyContent path using sharedLimiter copies data correctly.
340
	testData := "shared limiter content"
341
	src := strings.NewReader(testData)
342
	var dst strings.Builder
343
344
	downloader := &HTTPDownloader{
345
		sharedLimiter: rate.NewLimiter(rate.Limit(1<<20), 1<<20), // high limit to avoid slowness
346
	}
347
348
	done := make(chan bool)
349
	go downloader.copyContent(src, &dst, done)
350
	<-done
351
352
	if dst.String() != testData {
353
		t.Errorf("Expected content '%s', got '%s'", testData, dst.String())
354
	}
355
}
356
357
func TestProxyAwareHTTPClientConfiguration(t *testing.T) {
358
	// HTTP proxy config should set Transport.Proxy
359
	client := ProxyAwareHTTPClient("http://127.0.0.1:3128", false)
360
	tr, ok := client.Transport.(*http.Transport)
361
	if !ok {
362
		t.Fatalf("Transport is not *http.Transport")
363
	}
364
	if tr.Proxy == nil {
365
		t.Errorf("expected HTTP proxy to be configured on Transport.Proxy")
366
	}
367
368
	// SOCKS5 proxy config should set DialContext
369
	client = ProxyAwareHTTPClient("127.0.0.1:1080", false)
370
	tr, ok = client.Transport.(*http.Transport)
371
	if !ok || tr.DialContext == nil {
372
		t.Errorf("expected SOCKS5 DialContext to be configured")
373
	}
374
375
	// Ensure timeouts are set
376
	if tr.TLSHandshakeTimeout == 0 || tr.ResponseHeaderTimeout == 0 || tr.ExpectContinueTimeout == 0 {
377
		t.Errorf("expected transport timeouts to be set")
378
	}
379
}
380
381
func TestNewHTTPDownloaderProbe(t *testing.T) {
382
	// HEAD returns Accept-Ranges and Content-Length
383
	content := strings.Repeat("x", 1024)
384
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
385
		switch r.Method {
386
		case http.MethodHead:
387
			w.Header().Set("Accept-Ranges", "bytes")
388
			w.Header().Set("Content-Length", "1024")
389
			w.WriteHeader(http.StatusOK)
390
		case http.MethodGet:
391
			w.WriteHeader(http.StatusOK)
392
			_, _ = w.Write([]byte(content))
393
		}
394
	})
395
	ts := httptest.NewServer(h)
396
	defer ts.Close()
397
398
	d := NewHTTPDownloader(ts.URL, 4, false, "", "")
399
	if d.par != 4 {
400
		t.Fatalf("expected par=4, got %d", d.par)
401
	}
402
	if d.len != 1024 {
403
		t.Fatalf("expected len=1024, got %d", d.len)
404
	}
405
	if !d.resumable {
406
		t.Fatalf("expected resumable=true")
407
	}
408
	if len(d.parts) != 4 {
409
		t.Fatalf("expected 4 parts, got %d", len(d.parts))
410
	}
411
}
412
413
func TestNewHTTPDownloaderRangeFallback(t *testing.T) {
414
	// HEAD has no Accept-Ranges/Content-Length. GET with Range returns 206 + Content-Range
415
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
416
		switch r.Method {
417
		case http.MethodHead:
418
			w.WriteHeader(http.StatusOK)
419
		case http.MethodGet:
420
			if rng := r.Header.Get("Range"); strings.HasPrefix(rng, "bytes=") {
421
				w.Header().Set("Content-Range", "bytes 0-0/4096")
422
				w.WriteHeader(http.StatusPartialContent)
423
				_, _ = w.Write([]byte("x"))
424
				return
425
			}
426
			w.WriteHeader(http.StatusOK)
427
			_, _ = w.Write([]byte(strings.Repeat("x", 4096)))
428
		}
429
	})
430
	ts := httptest.NewServer(h)
431
	defer ts.Close()
432
433
	d := NewHTTPDownloader(ts.URL, 4, false, "", "")
434
	if d.par != 4 {
435
		t.Fatalf("expected par=4, got %d", d.par)
436
	}
437
	if d.len != 4096 {
438
		t.Fatalf("expected len=4096, got %d", d.len)
439
	}
440
}
441