Source file
src/net/http/fs_test.go
1
2
3
4
5 package http_test
6
7 import (
8 "bufio"
9 "bytes"
10 "errors"
11 "fmt"
12 "io"
13 "io/fs"
14 "mime"
15 "mime/multipart"
16 "net"
17 . "net/http"
18 "net/http/httptest"
19 "net/url"
20 "os"
21 "os/exec"
22 "path"
23 "path/filepath"
24 "reflect"
25 "regexp"
26 "runtime"
27 "strings"
28 "testing"
29 "time"
30 )
31
32 const (
33 testFile = "testdata/file"
34 testFileLen = 11
35 )
36
37 type wantRange struct {
38 start, end int64
39 }
40
41 var ServeFileRangeTests = []struct {
42 r string
43 code int
44 ranges []wantRange
45 }{
46 {r: "", code: StatusOK},
47 {r: "bytes=0-4", code: StatusPartialContent, ranges: []wantRange{{0, 5}}},
48 {r: "bytes=2-", code: StatusPartialContent, ranges: []wantRange{{2, testFileLen}}},
49 {r: "bytes=-5", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 5, testFileLen}}},
50 {r: "bytes=3-7", code: StatusPartialContent, ranges: []wantRange{{3, 8}}},
51 {r: "bytes=0-0,-2", code: StatusPartialContent, ranges: []wantRange{{0, 1}, {testFileLen - 2, testFileLen}}},
52 {r: "bytes=0-1,5-8", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, 9}}},
53 {r: "bytes=0-1,5-", code: StatusPartialContent, ranges: []wantRange{{0, 2}, {5, testFileLen}}},
54 {r: "bytes=5-1000", code: StatusPartialContent, ranges: []wantRange{{5, testFileLen}}},
55 {r: "bytes=0-,1-,2-,3-,4-", code: StatusOK},
56 {r: "bytes=0-9", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen - 1}}},
57 {r: "bytes=0-10", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
58 {r: "bytes=0-11", code: StatusPartialContent, ranges: []wantRange{{0, testFileLen}}},
59 {r: "bytes=10-11", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
60 {r: "bytes=10-", code: StatusPartialContent, ranges: []wantRange{{testFileLen - 1, testFileLen}}},
61 {r: "bytes=11-", code: StatusRequestedRangeNotSatisfiable},
62 {r: "bytes=11-12", code: StatusRequestedRangeNotSatisfiable},
63 {r: "bytes=12-12", code: StatusRequestedRangeNotSatisfiable},
64 {r: "bytes=11-100", code: StatusRequestedRangeNotSatisfiable},
65 {r: "bytes=12-100", code: StatusRequestedRangeNotSatisfiable},
66 {r: "bytes=100-", code: StatusRequestedRangeNotSatisfiable},
67 {r: "bytes=100-1000", code: StatusRequestedRangeNotSatisfiable},
68 }
69
70 func TestServeFile(t *testing.T) {
71 setParallel(t)
72 defer afterTest(t)
73 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
74 ServeFile(w, r, "testdata/file")
75 }))
76 defer ts.Close()
77 c := ts.Client()
78
79 var err error
80
81 file, err := os.ReadFile(testFile)
82 if err != nil {
83 t.Fatal("reading file:", err)
84 }
85
86
87 var req Request
88 req.Header = make(Header)
89 if req.URL, err = url.Parse(ts.URL); err != nil {
90 t.Fatal("ParseURL:", err)
91 }
92 req.Method = "GET"
93
94
95 _, body := getBody(t, "straight get", req, c)
96 if !bytes.Equal(body, file) {
97 t.Fatalf("body mismatch: got %q, want %q", body, file)
98 }
99
100
101 Cases:
102 for _, rt := range ServeFileRangeTests {
103 if rt.r != "" {
104 req.Header.Set("Range", rt.r)
105 }
106 resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req, c)
107 if resp.StatusCode != rt.code {
108 t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
109 }
110 if rt.code == StatusRequestedRangeNotSatisfiable {
111 continue
112 }
113 wantContentRange := ""
114 if len(rt.ranges) == 1 {
115 rng := rt.ranges[0]
116 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
117 }
118 cr := resp.Header.Get("Content-Range")
119 if cr != wantContentRange {
120 t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
121 }
122 ct := resp.Header.Get("Content-Type")
123 if len(rt.ranges) == 1 {
124 rng := rt.ranges[0]
125 wantBody := file[rng.start:rng.end]
126 if !bytes.Equal(body, wantBody) {
127 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
128 }
129 if strings.HasPrefix(ct, "multipart/byteranges") {
130 t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
131 }
132 }
133 if len(rt.ranges) > 1 {
134 typ, params, err := mime.ParseMediaType(ct)
135 if err != nil {
136 t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
137 continue
138 }
139 if typ != "multipart/byteranges" {
140 t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
141 continue
142 }
143 if params["boundary"] == "" {
144 t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
145 continue
146 }
147 if g, w := resp.ContentLength, int64(len(body)); g != w {
148 t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
149 continue
150 }
151 mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
152 for ri, rng := range rt.ranges {
153 part, err := mr.NextPart()
154 if err != nil {
155 t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
156 continue Cases
157 }
158 wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
159 if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
160 t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
161 }
162 body, err := io.ReadAll(part)
163 if err != nil {
164 t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
165 continue Cases
166 }
167 wantBody := file[rng.start:rng.end]
168 if !bytes.Equal(body, wantBody) {
169 t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
170 }
171 }
172 _, err = mr.NextPart()
173 if err != io.EOF {
174 t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
175 }
176 }
177 }
178 }
179
180 func TestServeFile_DotDot(t *testing.T) {
181 tests := []struct {
182 req string
183 wantStatus int
184 }{
185 {"/testdata/file", 200},
186 {"/../file", 400},
187 {"/..", 400},
188 {"/../", 400},
189 {"/../foo", 400},
190 {"/..\\foo", 400},
191 {"/file/a", 200},
192 {"/file/a..", 200},
193 {"/file/a/..", 400},
194 {"/file/a\\..", 400},
195 }
196 for _, tt := range tests {
197 req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
198 if err != nil {
199 t.Errorf("bad request %q: %v", tt.req, err)
200 continue
201 }
202 rec := httptest.NewRecorder()
203 ServeFile(rec, req, "testdata/file")
204 if rec.Code != tt.wantStatus {
205 t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
206 }
207 }
208 }
209
210
211 func TestServeFileDirPanicEmptyPath(t *testing.T) {
212 rec := httptest.NewRecorder()
213 req := httptest.NewRequest("GET", "/", nil)
214 req.URL.Path = ""
215 ServeFile(rec, req, "testdata")
216 res := rec.Result()
217 if res.StatusCode != 301 {
218 t.Errorf("code = %v; want 301", res.Status)
219 }
220 }
221
222 var fsRedirectTestData = []struct {
223 original, redirect string
224 }{
225 {"/test/index.html", "/test/"},
226 {"/test/testdata", "/test/testdata/"},
227 {"/test/testdata/file/", "/test/testdata/file"},
228 }
229
230 func TestFSRedirect(t *testing.T) {
231 defer afterTest(t)
232 ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
233 defer ts.Close()
234
235 for _, data := range fsRedirectTestData {
236 res, err := Get(ts.URL + data.original)
237 if err != nil {
238 t.Fatal(err)
239 }
240 res.Body.Close()
241 if g, e := res.Request.URL.Path, data.redirect; g != e {
242 t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
243 }
244 }
245 }
246
247 type testFileSystem struct {
248 open func(name string) (File, error)
249 }
250
251 func (fs *testFileSystem) Open(name string) (File, error) {
252 return fs.open(name)
253 }
254
255 func TestFileServerCleans(t *testing.T) {
256 defer afterTest(t)
257 ch := make(chan string, 1)
258 fs := FileServer(&testFileSystem{func(name string) (File, error) {
259 ch <- name
260 return nil, errors.New("file does not exist")
261 }})
262 tests := []struct {
263 reqPath, openArg string
264 }{
265 {"/foo.txt", "/foo.txt"},
266 {"//foo.txt", "/foo.txt"},
267 {"/../foo.txt", "/foo.txt"},
268 }
269 req, _ := NewRequest("GET", "http://example.com", nil)
270 for n, test := range tests {
271 rec := httptest.NewRecorder()
272 req.URL.Path = test.reqPath
273 fs.ServeHTTP(rec, req)
274 if got := <-ch; got != test.openArg {
275 t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
276 }
277 }
278 }
279
280 func TestFileServerEscapesNames(t *testing.T) {
281 defer afterTest(t)
282 const dirListPrefix = "<pre>\n"
283 const dirListSuffix = "\n</pre>\n"
284 tests := []struct {
285 name, escaped string
286 }{
287 {`simple_name`, `<a href="simple_name">simple_name</a>`},
288 {`"'<>&`, `<a href="%22%27%3C%3E&">"'<>&</a>`},
289 {`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
290 {`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo"><combo>?foo</a>`},
291 {`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
292 }
293
294
295 fs := make(fakeFS)
296 for i, test := range tests {
297 testFile := &fakeFileInfo{basename: test.name}
298 fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
299 dir: true,
300 modtime: time.Unix(1000000000, 0).UTC(),
301 ents: []*fakeFileInfo{testFile},
302 }
303 fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
304 }
305
306 ts := httptest.NewServer(FileServer(&fs))
307 defer ts.Close()
308 for i, test := range tests {
309 url := fmt.Sprintf("%s/%d", ts.URL, i)
310 res, err := Get(url)
311 if err != nil {
312 t.Fatalf("test %q: Get: %v", test.name, err)
313 }
314 b, err := io.ReadAll(res.Body)
315 if err != nil {
316 t.Fatalf("test %q: read Body: %v", test.name, err)
317 }
318 s := string(b)
319 if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
320 t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
321 }
322 if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
323 t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
324 }
325 res.Body.Close()
326 }
327 }
328
329 func TestFileServerSortsNames(t *testing.T) {
330 defer afterTest(t)
331 const contents = "I am a fake file"
332 dirMod := time.Unix(123, 0).UTC()
333 fileMod := time.Unix(1000000000, 0).UTC()
334 fs := fakeFS{
335 "/": &fakeFileInfo{
336 dir: true,
337 modtime: dirMod,
338 ents: []*fakeFileInfo{
339 {
340 basename: "b",
341 modtime: fileMod,
342 contents: contents,
343 },
344 {
345 basename: "a",
346 modtime: fileMod,
347 contents: contents,
348 },
349 },
350 },
351 }
352
353 ts := httptest.NewServer(FileServer(&fs))
354 defer ts.Close()
355
356 res, err := Get(ts.URL)
357 if err != nil {
358 t.Fatalf("Get: %v", err)
359 }
360 defer res.Body.Close()
361
362 b, err := io.ReadAll(res.Body)
363 if err != nil {
364 t.Fatalf("read Body: %v", err)
365 }
366 s := string(b)
367 if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
368 t.Errorf("output appears to be unsorted:\n%s", s)
369 }
370 }
371
372 func mustRemoveAll(dir string) {
373 err := os.RemoveAll(dir)
374 if err != nil {
375 panic(err)
376 }
377 }
378
379 func TestFileServerImplicitLeadingSlash(t *testing.T) {
380 defer afterTest(t)
381 tempDir := t.TempDir()
382 if err := os.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
383 t.Fatalf("WriteFile: %v", err)
384 }
385 ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
386 defer ts.Close()
387 get := func(suffix string) string {
388 res, err := Get(ts.URL + suffix)
389 if err != nil {
390 t.Fatalf("Get %s: %v", suffix, err)
391 }
392 b, err := io.ReadAll(res.Body)
393 if err != nil {
394 t.Fatalf("ReadAll %s: %v", suffix, err)
395 }
396 res.Body.Close()
397 return string(b)
398 }
399 if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
400 t.Logf("expected a directory listing with foo.txt, got %q", s)
401 }
402 if s := get("/bar/foo.txt"); s != "Hello world" {
403 t.Logf("expected %q, got %q", "Hello world", s)
404 }
405 }
406
407 func TestDirJoin(t *testing.T) {
408 if runtime.GOOS == "windows" {
409 t.Skip("skipping test on windows")
410 }
411 wfi, err := os.Stat("/etc/hosts")
412 if err != nil {
413 t.Skip("skipping test; no /etc/hosts file")
414 }
415 test := func(d Dir, name string) {
416 f, err := d.Open(name)
417 if err != nil {
418 t.Fatalf("open of %s: %v", name, err)
419 }
420 defer f.Close()
421 gfi, err := f.Stat()
422 if err != nil {
423 t.Fatalf("stat of %s: %v", name, err)
424 }
425 if !os.SameFile(gfi, wfi) {
426 t.Errorf("%s got different file", name)
427 }
428 }
429 test(Dir("/etc/"), "/hosts")
430 test(Dir("/etc/"), "hosts")
431 test(Dir("/etc/"), "../../../../hosts")
432 test(Dir("/etc"), "/hosts")
433 test(Dir("/etc"), "hosts")
434 test(Dir("/etc"), "../../../../hosts")
435
436
437
438 test(Dir("/etc/hosts"), "")
439 test(Dir("/etc/hosts"), "/")
440 test(Dir("/etc/hosts"), "../")
441 }
442
443 func TestEmptyDirOpenCWD(t *testing.T) {
444 test := func(d Dir) {
445 name := "fs_test.go"
446 f, err := d.Open(name)
447 if err != nil {
448 t.Fatalf("open of %s: %v", name, err)
449 }
450 defer f.Close()
451 }
452 test(Dir(""))
453 test(Dir("."))
454 test(Dir("./"))
455 }
456
457 func TestServeFileContentType(t *testing.T) {
458 defer afterTest(t)
459 const ctype = "icecream/chocolate"
460 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
461 switch r.FormValue("override") {
462 case "1":
463 w.Header().Set("Content-Type", ctype)
464 case "2":
465
466 w.Header()["Content-Type"] = []string{}
467 }
468 ServeFile(w, r, "testdata/file")
469 }))
470 defer ts.Close()
471 get := func(override string, want []string) {
472 resp, err := Get(ts.URL + "?override=" + override)
473 if err != nil {
474 t.Fatal(err)
475 }
476 if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
477 t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
478 }
479 resp.Body.Close()
480 }
481 get("0", []string{"text/plain; charset=utf-8"})
482 get("1", []string{ctype})
483 get("2", nil)
484 }
485
486 func TestServeFileMimeType(t *testing.T) {
487 defer afterTest(t)
488 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
489 ServeFile(w, r, "testdata/style.css")
490 }))
491 defer ts.Close()
492 resp, err := Get(ts.URL)
493 if err != nil {
494 t.Fatal(err)
495 }
496 resp.Body.Close()
497 want := "text/css; charset=utf-8"
498 if h := resp.Header.Get("Content-Type"); h != want {
499 t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
500 }
501 }
502
503 func TestServeFileFromCWD(t *testing.T) {
504 defer afterTest(t)
505 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
506 ServeFile(w, r, "fs_test.go")
507 }))
508 defer ts.Close()
509 r, err := Get(ts.URL)
510 if err != nil {
511 t.Fatal(err)
512 }
513 r.Body.Close()
514 if r.StatusCode != 200 {
515 t.Fatalf("expected 200 OK, got %s", r.Status)
516 }
517 }
518
519
520 func TestServeDirWithoutTrailingSlash(t *testing.T) {
521 e := "/testdata/"
522 defer afterTest(t)
523 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
524 ServeFile(w, r, ".")
525 }))
526 defer ts.Close()
527 r, err := Get(ts.URL + "/testdata")
528 if err != nil {
529 t.Fatal(err)
530 }
531 r.Body.Close()
532 if g := r.Request.URL.Path; g != e {
533 t.Errorf("got %s, want %s", g, e)
534 }
535 }
536
537
538
539 func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) }
540 func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) }
541 func testServeFileWithContentEncoding(t *testing.T, h2 bool) {
542 defer afterTest(t)
543 cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
544 w.Header().Set("Content-Encoding", "foo")
545 ServeFile(w, r, "testdata/file")
546
547
548
549
550
551
552
553
554 w.(Flusher).Flush()
555 }))
556 defer cst.close()
557 resp, err := cst.c.Get(cst.ts.URL)
558 if err != nil {
559 t.Fatal(err)
560 }
561 resp.Body.Close()
562 if g, e := resp.ContentLength, int64(-1); g != e {
563 t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
564 }
565 }
566
567 func TestServeIndexHtml(t *testing.T) {
568 defer afterTest(t)
569
570 for i := 0; i < 2; i++ {
571 var h Handler
572 var name string
573 switch i {
574 case 0:
575 h = FileServer(Dir("."))
576 name = "Dir"
577 case 1:
578 h = FileServer(FS(os.DirFS(".")))
579 name = "DirFS"
580 }
581 t.Run(name, func(t *testing.T) {
582 const want = "index.html says hello\n"
583 ts := httptest.NewServer(h)
584 defer ts.Close()
585
586 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
587 res, err := Get(ts.URL + path)
588 if err != nil {
589 t.Fatal(err)
590 }
591 b, err := io.ReadAll(res.Body)
592 if err != nil {
593 t.Fatal("reading Body:", err)
594 }
595 if s := string(b); s != want {
596 t.Errorf("for path %q got %q, want %q", path, s, want)
597 }
598 res.Body.Close()
599 }
600 })
601 }
602 }
603
604 func TestServeIndexHtmlFS(t *testing.T) {
605 defer afterTest(t)
606 const want = "index.html says hello\n"
607 ts := httptest.NewServer(FileServer(Dir(".")))
608 defer ts.Close()
609
610 for _, path := range []string{"/testdata/", "/testdata/index.html"} {
611 res, err := Get(ts.URL + path)
612 if err != nil {
613 t.Fatal(err)
614 }
615 b, err := io.ReadAll(res.Body)
616 if err != nil {
617 t.Fatal("reading Body:", err)
618 }
619 if s := string(b); s != want {
620 t.Errorf("for path %q got %q, want %q", path, s, want)
621 }
622 res.Body.Close()
623 }
624 }
625
626 func TestFileServerZeroByte(t *testing.T) {
627 defer afterTest(t)
628 ts := httptest.NewServer(FileServer(Dir(".")))
629 defer ts.Close()
630
631 c, err := net.Dial("tcp", ts.Listener.Addr().String())
632 if err != nil {
633 t.Fatal(err)
634 }
635 defer c.Close()
636 _, err = fmt.Fprintf(c, "GET /..\x00 HTTP/1.0\r\n\r\n")
637 if err != nil {
638 t.Fatal(err)
639 }
640 var got bytes.Buffer
641 bufr := bufio.NewReader(io.TeeReader(c, &got))
642 res, err := ReadResponse(bufr, nil)
643 if err != nil {
644 t.Fatal("ReadResponse: ", err)
645 }
646 if res.StatusCode == 200 {
647 t.Errorf("got status 200; want an error. Body is:\n%s", got.Bytes())
648 }
649 }
650
651 type fakeFileInfo struct {
652 dir bool
653 basename string
654 modtime time.Time
655 ents []*fakeFileInfo
656 contents string
657 err error
658 }
659
660 func (f *fakeFileInfo) Name() string { return f.basename }
661 func (f *fakeFileInfo) Sys() any { return nil }
662 func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
663 func (f *fakeFileInfo) IsDir() bool { return f.dir }
664 func (f *fakeFileInfo) Size() int64 { return int64(len(f.contents)) }
665 func (f *fakeFileInfo) Mode() fs.FileMode {
666 if f.dir {
667 return 0755 | fs.ModeDir
668 }
669 return 0644
670 }
671
672 type fakeFile struct {
673 io.ReadSeeker
674 fi *fakeFileInfo
675 path string
676 entpos int
677 }
678
679 func (f *fakeFile) Close() error { return nil }
680 func (f *fakeFile) Stat() (fs.FileInfo, error) { return f.fi, nil }
681 func (f *fakeFile) Readdir(count int) ([]fs.FileInfo, error) {
682 if !f.fi.dir {
683 return nil, fs.ErrInvalid
684 }
685 var fis []fs.FileInfo
686
687 limit := f.entpos + count
688 if count <= 0 || limit > len(f.fi.ents) {
689 limit = len(f.fi.ents)
690 }
691 for ; f.entpos < limit; f.entpos++ {
692 fis = append(fis, f.fi.ents[f.entpos])
693 }
694
695 if len(fis) == 0 && count > 0 {
696 return fis, io.EOF
697 } else {
698 return fis, nil
699 }
700 }
701
702 type fakeFS map[string]*fakeFileInfo
703
704 func (fsys fakeFS) Open(name string) (File, error) {
705 name = path.Clean(name)
706 f, ok := fsys[name]
707 if !ok {
708 return nil, fs.ErrNotExist
709 }
710 if f.err != nil {
711 return nil, f.err
712 }
713 return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
714 }
715
716 func TestDirectoryIfNotModified(t *testing.T) {
717 defer afterTest(t)
718 const indexContents = "I am a fake index.html file"
719 fileMod := time.Unix(1000000000, 0).UTC()
720 fileModStr := fileMod.Format(TimeFormat)
721 dirMod := time.Unix(123, 0).UTC()
722 indexFile := &fakeFileInfo{
723 basename: "index.html",
724 modtime: fileMod,
725 contents: indexContents,
726 }
727 fs := fakeFS{
728 "/": &fakeFileInfo{
729 dir: true,
730 modtime: dirMod,
731 ents: []*fakeFileInfo{indexFile},
732 },
733 "/index.html": indexFile,
734 }
735
736 ts := httptest.NewServer(FileServer(fs))
737 defer ts.Close()
738
739 res, err := Get(ts.URL)
740 if err != nil {
741 t.Fatal(err)
742 }
743 b, err := io.ReadAll(res.Body)
744 if err != nil {
745 t.Fatal(err)
746 }
747 if string(b) != indexContents {
748 t.Fatalf("Got body %q; want %q", b, indexContents)
749 }
750 res.Body.Close()
751
752 lastMod := res.Header.Get("Last-Modified")
753 if lastMod != fileModStr {
754 t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
755 }
756
757 req, _ := NewRequest("GET", ts.URL, nil)
758 req.Header.Set("If-Modified-Since", lastMod)
759
760 c := ts.Client()
761 res, err = c.Do(req)
762 if err != nil {
763 t.Fatal(err)
764 }
765 if res.StatusCode != 304 {
766 t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
767 }
768 res.Body.Close()
769
770
771 indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
772
773 res, err = c.Do(req)
774 if err != nil {
775 t.Fatal(err)
776 }
777 if res.StatusCode != 200 {
778 t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
779 }
780 res.Body.Close()
781 }
782
783 func mustStat(t *testing.T, fileName string) fs.FileInfo {
784 fi, err := os.Stat(fileName)
785 if err != nil {
786 t.Fatal(err)
787 }
788 return fi
789 }
790
791 func TestServeContent(t *testing.T) {
792 defer afterTest(t)
793 type serveParam struct {
794 name string
795 modtime time.Time
796 content io.ReadSeeker
797 contentType string
798 etag string
799 }
800 servec := make(chan serveParam, 1)
801 ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
802 p := <-servec
803 if p.etag != "" {
804 w.Header().Set("ETag", p.etag)
805 }
806 if p.contentType != "" {
807 w.Header().Set("Content-Type", p.contentType)
808 }
809 ServeContent(w, r, p.name, p.modtime, p.content)
810 }))
811 defer ts.Close()
812
813 type testCase struct {
814
815 file string
816 content io.ReadSeeker
817
818 modtime time.Time
819 serveETag string
820 serveContentType string
821 reqHeader map[string]string
822 wantLastMod string
823 wantContentType string
824 wantContentRange string
825 wantStatus int
826 }
827 htmlModTime := mustStat(t, "testdata/index.html").ModTime()
828 tests := map[string]testCase{
829 "no_last_modified": {
830 file: "testdata/style.css",
831 wantContentType: "text/css; charset=utf-8",
832 wantStatus: 200,
833 },
834 "with_last_modified": {
835 file: "testdata/index.html",
836 wantContentType: "text/html; charset=utf-8",
837 modtime: htmlModTime,
838 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
839 wantStatus: 200,
840 },
841 "not_modified_modtime": {
842 file: "testdata/style.css",
843 serveETag: `"foo"`,
844 modtime: htmlModTime,
845 reqHeader: map[string]string{
846 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
847 },
848 wantStatus: 304,
849 },
850 "not_modified_modtime_with_contenttype": {
851 file: "testdata/style.css",
852 serveContentType: "text/css",
853 serveETag: `"foo"`,
854 modtime: htmlModTime,
855 reqHeader: map[string]string{
856 "If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
857 },
858 wantStatus: 304,
859 },
860 "not_modified_etag": {
861 file: "testdata/style.css",
862 serveETag: `"foo"`,
863 reqHeader: map[string]string{
864 "If-None-Match": `"foo"`,
865 },
866 wantStatus: 304,
867 },
868 "not_modified_etag_no_seek": {
869 content: panicOnSeek{nil},
870 serveETag: `W/"foo"`,
871 reqHeader: map[string]string{
872 "If-None-Match": `"baz", W/"foo"`,
873 },
874 wantStatus: 304,
875 },
876 "if_none_match_mismatch": {
877 file: "testdata/style.css",
878 serveETag: `"foo"`,
879 reqHeader: map[string]string{
880 "If-None-Match": `"Foo"`,
881 },
882 wantStatus: 200,
883 wantContentType: "text/css; charset=utf-8",
884 },
885 "if_none_match_malformed": {
886 file: "testdata/style.css",
887 serveETag: `"foo"`,
888 reqHeader: map[string]string{
889 "If-None-Match": `,`,
890 },
891 wantStatus: 200,
892 wantContentType: "text/css; charset=utf-8",
893 },
894 "range_good": {
895 file: "testdata/style.css",
896 serveETag: `"A"`,
897 reqHeader: map[string]string{
898 "Range": "bytes=0-4",
899 },
900 wantStatus: StatusPartialContent,
901 wantContentType: "text/css; charset=utf-8",
902 wantContentRange: "bytes 0-4/8",
903 },
904 "range_match": {
905 file: "testdata/style.css",
906 serveETag: `"A"`,
907 reqHeader: map[string]string{
908 "Range": "bytes=0-4",
909 "If-Range": `"A"`,
910 },
911 wantStatus: StatusPartialContent,
912 wantContentType: "text/css; charset=utf-8",
913 wantContentRange: "bytes 0-4/8",
914 },
915 "range_match_weak_etag": {
916 file: "testdata/style.css",
917 serveETag: `W/"A"`,
918 reqHeader: map[string]string{
919 "Range": "bytes=0-4",
920 "If-Range": `W/"A"`,
921 },
922 wantStatus: 200,
923 wantContentType: "text/css; charset=utf-8",
924 },
925 "range_no_overlap": {
926 file: "testdata/style.css",
927 serveETag: `"A"`,
928 reqHeader: map[string]string{
929 "Range": "bytes=10-20",
930 },
931 wantStatus: StatusRequestedRangeNotSatisfiable,
932 wantContentType: "text/plain; charset=utf-8",
933 wantContentRange: "bytes */8",
934 },
935
936
937 "range_no_match": {
938 file: "testdata/style.css",
939 serveETag: `"A"`,
940 reqHeader: map[string]string{
941 "Range": "bytes=0-4",
942 "If-Range": `"B"`,
943 },
944 wantStatus: 200,
945 wantContentType: "text/css; charset=utf-8",
946 },
947 "range_with_modtime": {
948 file: "testdata/style.css",
949 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 , time.UTC),
950 reqHeader: map[string]string{
951 "Range": "bytes=0-4",
952 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
953 },
954 wantStatus: StatusPartialContent,
955 wantContentType: "text/css; charset=utf-8",
956 wantContentRange: "bytes 0-4/8",
957 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
958 },
959 "range_with_modtime_mismatch": {
960 file: "testdata/style.css",
961 modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 , time.UTC),
962 reqHeader: map[string]string{
963 "Range": "bytes=0-4",
964 "If-Range": "Wed, 25 Jun 2014 17:12:19 GMT",
965 },
966 wantStatus: StatusOK,
967 wantContentType: "text/css; charset=utf-8",
968 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
969 },
970 "range_with_modtime_nanos": {
971 file: "testdata/style.css",
972 modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 , time.UTC),
973 reqHeader: map[string]string{
974 "Range": "bytes=0-4",
975 "If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
976 },
977 wantStatus: StatusPartialContent,
978 wantContentType: "text/css; charset=utf-8",
979 wantContentRange: "bytes 0-4/8",
980 wantLastMod: "Wed, 25 Jun 2014 17:12:18 GMT",
981 },
982 "unix_zero_modtime": {
983 content: strings.NewReader("<html>foo"),
984 modtime: time.Unix(0, 0),
985 wantStatus: StatusOK,
986 wantContentType: "text/html; charset=utf-8",
987 },
988 "ifmatch_matches": {
989 file: "testdata/style.css",
990 serveETag: `"A"`,
991 reqHeader: map[string]string{
992 "If-Match": `"Z", "A"`,
993 },
994 wantStatus: 200,
995 wantContentType: "text/css; charset=utf-8",
996 },
997 "ifmatch_star": {
998 file: "testdata/style.css",
999 serveETag: `"A"`,
1000 reqHeader: map[string]string{
1001 "If-Match": `*`,
1002 },
1003 wantStatus: 200,
1004 wantContentType: "text/css; charset=utf-8",
1005 },
1006 "ifmatch_failed": {
1007 file: "testdata/style.css",
1008 serveETag: `"A"`,
1009 reqHeader: map[string]string{
1010 "If-Match": `"B"`,
1011 },
1012 wantStatus: 412,
1013 },
1014 "ifmatch_fails_on_weak_etag": {
1015 file: "testdata/style.css",
1016 serveETag: `W/"A"`,
1017 reqHeader: map[string]string{
1018 "If-Match": `W/"A"`,
1019 },
1020 wantStatus: 412,
1021 },
1022 "if_unmodified_since_true": {
1023 file: "testdata/style.css",
1024 modtime: htmlModTime,
1025 reqHeader: map[string]string{
1026 "If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
1027 },
1028 wantStatus: 200,
1029 wantContentType: "text/css; charset=utf-8",
1030 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1031 },
1032 "if_unmodified_since_false": {
1033 file: "testdata/style.css",
1034 modtime: htmlModTime,
1035 reqHeader: map[string]string{
1036 "If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
1037 },
1038 wantStatus: 412,
1039 wantLastMod: htmlModTime.UTC().Format(TimeFormat),
1040 },
1041 }
1042 for testName, tt := range tests {
1043 var content io.ReadSeeker
1044 if tt.file != "" {
1045 f, err := os.Open(tt.file)
1046 if err != nil {
1047 t.Fatalf("test %q: %v", testName, err)
1048 }
1049 defer f.Close()
1050 content = f
1051 } else {
1052 content = tt.content
1053 }
1054 for _, method := range []string{"GET", "HEAD"} {
1055
1056 if content, ok := content.(*strings.Reader); ok {
1057 content.Seek(0, io.SeekStart)
1058 }
1059
1060 servec <- serveParam{
1061 name: filepath.Base(tt.file),
1062 content: content,
1063 modtime: tt.modtime,
1064 etag: tt.serveETag,
1065 contentType: tt.serveContentType,
1066 }
1067 req, err := NewRequest(method, ts.URL, nil)
1068 if err != nil {
1069 t.Fatal(err)
1070 }
1071 for k, v := range tt.reqHeader {
1072 req.Header.Set(k, v)
1073 }
1074
1075 c := ts.Client()
1076 res, err := c.Do(req)
1077 if err != nil {
1078 t.Fatal(err)
1079 }
1080 io.Copy(io.Discard, res.Body)
1081 res.Body.Close()
1082 if res.StatusCode != tt.wantStatus {
1083 t.Errorf("test %q using %q: got status = %d; want %d", testName, method, res.StatusCode, tt.wantStatus)
1084 }
1085 if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
1086 t.Errorf("test %q using %q: got content-type = %q, want %q", testName, method, g, e)
1087 }
1088 if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
1089 t.Errorf("test %q using %q: got content-range = %q, want %q", testName, method, g, e)
1090 }
1091 if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
1092 t.Errorf("test %q using %q: got last-modified = %q, want %q", testName, method, g, e)
1093 }
1094 }
1095 }
1096 }
1097
1098
1099 func TestServerFileStatError(t *testing.T) {
1100 rec := httptest.NewRecorder()
1101 r, _ := NewRequest("GET", "http://foo/", nil)
1102 redirect := false
1103 name := "file.txt"
1104 fs := issue12991FS{}
1105 ExportServeFile(rec, r, fs, name, redirect)
1106 if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
1107 t.Errorf("wanted 403 forbidden message; got: %s", body)
1108 }
1109 }
1110
1111 type issue12991FS struct{}
1112
1113 func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
1114
1115 type issue12991File struct{ File }
1116
1117 func (issue12991File) Stat() (fs.FileInfo, error) { return nil, fs.ErrPermission }
1118 func (issue12991File) Close() error { return nil }
1119
1120 func TestServeContentErrorMessages(t *testing.T) {
1121 defer afterTest(t)
1122 fs := fakeFS{
1123 "/500": &fakeFileInfo{
1124 err: errors.New("random error"),
1125 },
1126 "/403": &fakeFileInfo{
1127 err: &fs.PathError{Err: fs.ErrPermission},
1128 },
1129 }
1130 ts := httptest.NewServer(FileServer(fs))
1131 defer ts.Close()
1132 c := ts.Client()
1133 for _, code := range []int{403, 404, 500} {
1134 res, err := c.Get(fmt.Sprintf("%s/%d", ts.URL, code))
1135 if err != nil {
1136 t.Errorf("Error fetching /%d: %v", code, err)
1137 continue
1138 }
1139 if res.StatusCode != code {
1140 t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
1141 }
1142 res.Body.Close()
1143 }
1144 }
1145
1146
1147 func TestLinuxSendfile(t *testing.T) {
1148 setParallel(t)
1149 defer afterTest(t)
1150 if runtime.GOOS != "linux" {
1151 t.Skip("skipping; linux-only test")
1152 }
1153 if _, err := exec.LookPath("strace"); err != nil {
1154 t.Skip("skipping; strace not found in path")
1155 }
1156
1157 ln, err := net.Listen("tcp", "127.0.0.1:0")
1158 if err != nil {
1159 t.Fatal(err)
1160 }
1161 lnf, err := ln.(*net.TCPListener).File()
1162 if err != nil {
1163 t.Fatal(err)
1164 }
1165 defer ln.Close()
1166
1167
1168 if err := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=^$").Run(); err != nil {
1169 t.Skipf("skipping; failed to run strace: %v", err)
1170 }
1171
1172 filename := fmt.Sprintf("1kb-%d", os.Getpid())
1173 filepath := path.Join(os.TempDir(), filename)
1174
1175 if err := os.WriteFile(filepath, bytes.Repeat([]byte{'a'}, 1<<10), 0755); err != nil {
1176 t.Fatal(err)
1177 }
1178 defer os.Remove(filepath)
1179
1180 var buf bytes.Buffer
1181 child := exec.Command("strace", "-f", "-q", os.Args[0], "-test.run=TestLinuxSendfileChild")
1182 child.ExtraFiles = append(child.ExtraFiles, lnf)
1183 child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
1184 child.Stdout = &buf
1185 child.Stderr = &buf
1186 if err := child.Start(); err != nil {
1187 t.Skipf("skipping; failed to start straced child: %v", err)
1188 }
1189
1190 res, err := Get(fmt.Sprintf("http://%s/%s", ln.Addr(), filename))
1191 if err != nil {
1192 t.Fatalf("http client error: %v", err)
1193 }
1194 _, err = io.Copy(io.Discard, res.Body)
1195 if err != nil {
1196 t.Fatalf("client body read error: %v", err)
1197 }
1198 res.Body.Close()
1199
1200
1201 Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
1202 child.Wait()
1203
1204 rx := regexp.MustCompile(`\b(n64:)?sendfile(64)?\(`)
1205 out := buf.String()
1206 if !rx.MatchString(out) {
1207 t.Errorf("no sendfile system call found in:\n%s", out)
1208 }
1209 }
1210
1211 func getBody(t *testing.T, testName string, req Request, client *Client) (*Response, []byte) {
1212 r, err := client.Do(&req)
1213 if err != nil {
1214 t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
1215 }
1216 b, err := io.ReadAll(r.Body)
1217 if err != nil {
1218 t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
1219 }
1220 return r, b
1221 }
1222
1223
1224
1225 func TestLinuxSendfileChild(*testing.T) {
1226 if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
1227 return
1228 }
1229 defer os.Exit(0)
1230 fd3 := os.NewFile(3, "ephemeral-port-listener")
1231 ln, err := net.FileListener(fd3)
1232 if err != nil {
1233 panic(err)
1234 }
1235 mux := NewServeMux()
1236 mux.Handle("/", FileServer(Dir(os.TempDir())))
1237 mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
1238 os.Exit(0)
1239 })
1240 s := &Server{Handler: mux}
1241 err = s.Serve(ln)
1242 if err != nil {
1243 panic(err)
1244 }
1245 }
1246
1247
1248 func TestFileServerNotDirError(t *testing.T) {
1249 defer afterTest(t)
1250 t.Run("Dir", func(t *testing.T) {
1251 testFileServerNotDirError(t, func(path string) FileSystem { return Dir(path) })
1252 })
1253 t.Run("FS", func(t *testing.T) {
1254 testFileServerNotDirError(t, func(path string) FileSystem { return FS(os.DirFS(path)) })
1255 })
1256 }
1257
1258 func testFileServerNotDirError(t *testing.T, newfs func(string) FileSystem) {
1259 ts := httptest.NewServer(FileServer(newfs("testdata")))
1260 defer ts.Close()
1261
1262 res, err := Get(ts.URL + "/index.html/not-a-file")
1263 if err != nil {
1264 t.Fatal(err)
1265 }
1266 res.Body.Close()
1267 if res.StatusCode != 404 {
1268 t.Errorf("StatusCode = %v; want 404", res.StatusCode)
1269 }
1270
1271 test := func(name string, fsys FileSystem) {
1272 t.Run(name, func(t *testing.T) {
1273 _, err = fsys.Open("/index.html/not-a-file")
1274 if err == nil {
1275 t.Fatal("err == nil; want != nil")
1276 }
1277 if !errors.Is(err, fs.ErrNotExist) {
1278 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1279 errors.Is(err, fs.ErrNotExist))
1280 }
1281
1282 _, err = fsys.Open("/index.html/not-a-dir/not-a-file")
1283 if err == nil {
1284 t.Fatal("err == nil; want != nil")
1285 }
1286 if !errors.Is(err, fs.ErrNotExist) {
1287 t.Errorf("err = %v; errors.Is(err, fs.ErrNotExist) = %v; want true", err,
1288 errors.Is(err, fs.ErrNotExist))
1289 }
1290 })
1291 }
1292
1293 absPath, err := filepath.Abs("testdata")
1294 if err != nil {
1295 t.Fatal("get abs path:", err)
1296 }
1297
1298 test("RelativePath", newfs("testdata"))
1299 test("AbsolutePath", newfs(absPath))
1300 }
1301
1302 func TestFileServerCleanPath(t *testing.T) {
1303 tests := []struct {
1304 path string
1305 wantCode int
1306 wantOpen []string
1307 }{
1308 {"/", 200, []string{"/", "/index.html"}},
1309 {"/dir", 301, []string{"/dir"}},
1310 {"/dir/", 200, []string{"/dir", "/dir/index.html"}},
1311 }
1312 for _, tt := range tests {
1313 var log []string
1314 rr := httptest.NewRecorder()
1315 req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
1316 FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
1317 if !reflect.DeepEqual(log, tt.wantOpen) {
1318 t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
1319 }
1320 if rr.Code != tt.wantCode {
1321 t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
1322 }
1323 }
1324 }
1325
1326 type fileServerCleanPathDir struct {
1327 log *[]string
1328 }
1329
1330 func (d fileServerCleanPathDir) Open(path string) (File, error) {
1331 *(d.log) = append(*(d.log), path)
1332 if path == "/" || path == "/dir" || path == "/dir/" {
1333
1334 return Dir(".").Open(".")
1335 }
1336 return nil, fs.ErrNotExist
1337 }
1338
1339 type panicOnSeek struct{ io.ReadSeeker }
1340
1341 func Test_scanETag(t *testing.T) {
1342 tests := []struct {
1343 in string
1344 wantETag string
1345 wantRemain string
1346 }{
1347 {`W/"etag-1"`, `W/"etag-1"`, ""},
1348 {`"etag-2"`, `"etag-2"`, ""},
1349 {`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
1350 {"", "", ""},
1351 {"W/", "", ""},
1352 {`W/"truc`, "", ""},
1353 {`w/"case-sensitive"`, "", ""},
1354 {`"spaced etag"`, "", ""},
1355 }
1356 for _, test := range tests {
1357 etag, remain := ExportScanETag(test.in)
1358 if etag != test.wantETag || remain != test.wantRemain {
1359 t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
1360 }
1361 }
1362 }
1363
1364
1365
1366 func TestServeFileRejectsInvalidSuffixLengths_h1(t *testing.T) {
1367 testServeFileRejectsInvalidSuffixLengths(t, h1Mode)
1368 }
1369 func TestServeFileRejectsInvalidSuffixLengths_h2(t *testing.T) {
1370 testServeFileRejectsInvalidSuffixLengths(t, h2Mode)
1371 }
1372
1373 func testServeFileRejectsInvalidSuffixLengths(t *testing.T, h2 bool) {
1374 defer afterTest(t)
1375 cst := httptest.NewUnstartedServer(FileServer(Dir("testdata")))
1376 cst.EnableHTTP2 = h2
1377 cst.StartTLS()
1378 defer cst.Close()
1379
1380 tests := []struct {
1381 r string
1382 wantCode int
1383 wantBody string
1384 }{
1385 {"bytes=--6", 416, "invalid range\n"},
1386 {"bytes=--0", 416, "invalid range\n"},
1387 {"bytes=---0", 416, "invalid range\n"},
1388 {"bytes=-6", 206, "hello\n"},
1389 {"bytes=6-", 206, "html says hello\n"},
1390 {"bytes=-6-", 416, "invalid range\n"},
1391 {"bytes=-0", 206, ""},
1392 {"bytes=", 200, "index.html says hello\n"},
1393 }
1394
1395 for _, tt := range tests {
1396 tt := tt
1397 t.Run(tt.r, func(t *testing.T) {
1398 req, err := NewRequest("GET", cst.URL+"/index.html", nil)
1399 if err != nil {
1400 t.Fatal(err)
1401 }
1402 req.Header.Set("Range", tt.r)
1403 res, err := cst.Client().Do(req)
1404 if err != nil {
1405 t.Fatal(err)
1406 }
1407 if g, w := res.StatusCode, tt.wantCode; g != w {
1408 t.Errorf("StatusCode mismatch: got %d want %d", g, w)
1409 }
1410 slurp, err := io.ReadAll(res.Body)
1411 res.Body.Close()
1412 if err != nil {
1413 t.Fatal(err)
1414 }
1415 if g, w := string(slurp), tt.wantBody; g != w {
1416 t.Fatalf("Content mismatch:\nGot: %q\nWant: %q", g, w)
1417 }
1418 })
1419 }
1420 }
1421
View as plain text