package httpdl import ( "os" "path/filepath" "testing" ) func TestMakeSegments(t *testing.T) { tests := []struct { name string total int64 minSplit int64 conns int wantN int }{ {"even split", 1000, 100, 5, 5}, {"min-split caps count", 1000, 400, 5, 2}, {"tiny file is one segment", 50, 100, 5, 1}, {"single connection", 1000, 1, 1, 1}, {"min-split exact", 1000, 200, 5, 5}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { segs := makeSegments(tc.total, tc.minSplit, tc.conns) if len(segs) != tc.wantN { t.Fatalf("got %d segments, want %d", len(segs), tc.wantN) } // Segments must be contiguous and cover the whole file exactly. var next int64 for i, s := range segs { if s.start != next { t.Errorf("segment %d starts at %d, want %d", i, s.start, next) } if s.end < s.start { t.Errorf("segment %d has end %d < start %d", i, s.end, s.start) } next = s.end + 1 } if next != tc.total { t.Errorf("segments cover %d bytes, want %d", next, tc.total) } }) } } func TestSegProgress(t *testing.T) { s := seg{start: 100, end: 199} // length 100 if s.length() != 100 { t.Fatalf("length = %d", s.length()) } if s.done() { t.Fatalf("new segment should not be done") } s.advance(60) if s.offset() != 160 { t.Errorf("offset = %d, want 160", s.offset()) } if s.remaining() != 40 { t.Errorf("remaining = %d, want 40", s.remaining()) } s.advance(40) if !s.done() { t.Errorf("segment should be done after writing full length") } } func TestSplitExt(t *testing.T) { tests := []struct { in string stem, ext string }{ {"foo.txt", "foo", ".txt"}, {"foo", "foo", ""}, {"/dir/foo.tar.gz", "/dir/foo.tar", ".gz"}, {"/dir/.bashrc", "/dir/.bashrc", ""}, // dotfile, no extension {".bashrc", ".bashrc", ""}, {"archive.", "archive", "."}, } for _, tc := range tests { stem, ext := splitExt(tc.in) if stem != tc.stem || ext != tc.ext { t.Errorf("splitExt(%q) = %q,%q; want %q,%q", tc.in, stem, ext, tc.stem, tc.ext) } } } func TestUniqueName(t *testing.T) { dir := t.TempDir() // foo.txt taken -> foo.1.txt (.N inserted before the extension, item [13]). taken := filepath.Join(dir, "foo.txt") if err := os.WriteFile(taken, nil, 0o644); err != nil { t.Fatal(err) } got, err := uniqueName(taken) if err != nil { t.Fatalf("uniqueName: %v", err) } if want := filepath.Join(dir, "foo.1.txt"); got != want { t.Errorf("uniqueName(%q) = %q, want %q", taken, got, want) } // No-extension base -> base.1. plain := filepath.Join(dir, "data") if err := os.WriteFile(plain, nil, 0o644); err != nil { t.Fatal(err) } got, err = uniqueName(plain) if err != nil { t.Fatalf("uniqueName: %v", err) } if want := filepath.Join(dir, "data.1"); got != want { t.Errorf("uniqueName(%q) = %q, want %q", plain, got, want) } // foo.txt and foo.1.txt both taken -> foo.2.txt. if err := os.WriteFile(filepath.Join(dir, "foo.1.txt"), nil, 0o644); err != nil { t.Fatal(err) } got, err = uniqueName(taken) if err != nil { t.Fatalf("uniqueName: %v", err) } if want := filepath.Join(dir, "foo.2.txt"); got != want { t.Errorf("uniqueName second pass = %q, want %q", got, want) } // A candidate that exists but has a .got control file is acceptable (it is a // resumable partial). ctrlBase := filepath.Join(dir, "r.bin") cand1 := filepath.Join(dir, "r.1.bin") if err := os.WriteFile(ctrlBase, nil, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(cand1, nil, 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(controlPath(cand1), nil, 0o644); err != nil { t.Fatal(err) } got, err = uniqueName(ctrlBase) if err != nil { t.Fatalf("uniqueName: %v", err) } if got != cand1 { t.Errorf("uniqueName with control sidecar = %q, want %q", got, cand1) } } func TestNameFromContentDisposition(t *testing.T) { tests := []struct { cd, want string }{ {`attachment; filename="report.pdf"`, "report.pdf"}, {`attachment; filename=report.pdf`, "report.pdf"}, {`inline; filename*=UTF-8''na%C3%AFve.txt`, "naïve.txt"}, {`attachment; filename="../../etc/passwd"`, "passwd"}, // path stripped {`attachment`, ""}, {`garbage`, ""}, } for _, tc := range tests { if got := nameFromContentDisposition(tc.cd); got != tc.want { t.Errorf("nameFromContentDisposition(%q) = %q, want %q", tc.cd, got, tc.want) } } } func TestSeedSegmentsFromPrefix(t *testing.T) { segs := makeSegments(1000, 100, 5) // five 200-byte segments seedSegmentsFromPrefix(segs, 450) // 2 full segments + 50 bytes of the third wants := []int64{200, 200, 50, 0, 0} for i := range segs { if segs[i].written != wants[i] { t.Errorf("segment %d written = %d, want %d", i, segs[i].written, wants[i]) } } } func TestParseContentRange(t *testing.T) { start, end, total, ok := parseContentRange("bytes 0-0/12345") if !ok || start != 0 || end != 0 || total != 12345 { t.Errorf("parseContentRange = %d,%d,%d,%v", start, end, total, ok) } if _, _, _, ok := parseContentRange("garbage"); ok { t.Errorf("parseContentRange(garbage) ok = true, want false") } }