github.com/riscv/riscv-go@v0.0.0-20200123204226-124ebd6fcc8e/src/net/http/fs_test.go (about)

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package http_test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"io/ioutil"
    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 // range [start,end)
    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}, // ignore wasteful range request
    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  
    78  	var err error
    79  
    80  	file, err := ioutil.ReadFile(testFile)
    81  	if err != nil {
    82  		t.Fatal("reading file:", err)
    83  	}
    84  
    85  	// set up the Request (re-used for all tests)
    86  	var req Request
    87  	req.Header = make(Header)
    88  	if req.URL, err = url.Parse(ts.URL); err != nil {
    89  		t.Fatal("ParseURL:", err)
    90  	}
    91  	req.Method = "GET"
    92  
    93  	// straight GET
    94  	_, body := getBody(t, "straight get", req)
    95  	if !bytes.Equal(body, file) {
    96  		t.Fatalf("body mismatch: got %q, want %q", body, file)
    97  	}
    98  
    99  	// Range tests
   100  Cases:
   101  	for _, rt := range ServeFileRangeTests {
   102  		if rt.r != "" {
   103  			req.Header.Set("Range", rt.r)
   104  		}
   105  		resp, body := getBody(t, fmt.Sprintf("range test %q", rt.r), req)
   106  		if resp.StatusCode != rt.code {
   107  			t.Errorf("range=%q: StatusCode=%d, want %d", rt.r, resp.StatusCode, rt.code)
   108  		}
   109  		if rt.code == StatusRequestedRangeNotSatisfiable {
   110  			continue
   111  		}
   112  		wantContentRange := ""
   113  		if len(rt.ranges) == 1 {
   114  			rng := rt.ranges[0]
   115  			wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   116  		}
   117  		cr := resp.Header.Get("Content-Range")
   118  		if cr != wantContentRange {
   119  			t.Errorf("range=%q: Content-Range = %q, want %q", rt.r, cr, wantContentRange)
   120  		}
   121  		ct := resp.Header.Get("Content-Type")
   122  		if len(rt.ranges) == 1 {
   123  			rng := rt.ranges[0]
   124  			wantBody := file[rng.start:rng.end]
   125  			if !bytes.Equal(body, wantBody) {
   126  				t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   127  			}
   128  			if strings.HasPrefix(ct, "multipart/byteranges") {
   129  				t.Errorf("range=%q content-type = %q; unexpected multipart/byteranges", rt.r, ct)
   130  			}
   131  		}
   132  		if len(rt.ranges) > 1 {
   133  			typ, params, err := mime.ParseMediaType(ct)
   134  			if err != nil {
   135  				t.Errorf("range=%q content-type = %q; %v", rt.r, ct, err)
   136  				continue
   137  			}
   138  			if typ != "multipart/byteranges" {
   139  				t.Errorf("range=%q content-type = %q; want multipart/byteranges", rt.r, typ)
   140  				continue
   141  			}
   142  			if params["boundary"] == "" {
   143  				t.Errorf("range=%q content-type = %q; lacks boundary", rt.r, ct)
   144  				continue
   145  			}
   146  			if g, w := resp.ContentLength, int64(len(body)); g != w {
   147  				t.Errorf("range=%q Content-Length = %d; want %d", rt.r, g, w)
   148  				continue
   149  			}
   150  			mr := multipart.NewReader(bytes.NewReader(body), params["boundary"])
   151  			for ri, rng := range rt.ranges {
   152  				part, err := mr.NextPart()
   153  				if err != nil {
   154  					t.Errorf("range=%q, reading part index %d: %v", rt.r, ri, err)
   155  					continue Cases
   156  				}
   157  				wantContentRange = fmt.Sprintf("bytes %d-%d/%d", rng.start, rng.end-1, testFileLen)
   158  				if g, w := part.Header.Get("Content-Range"), wantContentRange; g != w {
   159  					t.Errorf("range=%q: part Content-Range = %q; want %q", rt.r, g, w)
   160  				}
   161  				body, err := ioutil.ReadAll(part)
   162  				if err != nil {
   163  					t.Errorf("range=%q, reading part index %d body: %v", rt.r, ri, err)
   164  					continue Cases
   165  				}
   166  				wantBody := file[rng.start:rng.end]
   167  				if !bytes.Equal(body, wantBody) {
   168  					t.Errorf("range=%q: body = %q, want %q", rt.r, body, wantBody)
   169  				}
   170  			}
   171  			_, err = mr.NextPart()
   172  			if err != io.EOF {
   173  				t.Errorf("range=%q; expected final error io.EOF; got %v", rt.r, err)
   174  			}
   175  		}
   176  	}
   177  }
   178  
   179  func TestServeFile_DotDot(t *testing.T) {
   180  	tests := []struct {
   181  		req        string
   182  		wantStatus int
   183  	}{
   184  		{"/testdata/file", 200},
   185  		{"/../file", 400},
   186  		{"/..", 400},
   187  		{"/../", 400},
   188  		{"/../foo", 400},
   189  		{"/..\\foo", 400},
   190  		{"/file/a", 200},
   191  		{"/file/a..", 200},
   192  		{"/file/a/..", 400},
   193  		{"/file/a\\..", 400},
   194  	}
   195  	for _, tt := range tests {
   196  		req, err := ReadRequest(bufio.NewReader(strings.NewReader("GET " + tt.req + " HTTP/1.1\r\nHost: foo\r\n\r\n")))
   197  		if err != nil {
   198  			t.Errorf("bad request %q: %v", tt.req, err)
   199  			continue
   200  		}
   201  		rec := httptest.NewRecorder()
   202  		ServeFile(rec, req, "testdata/file")
   203  		if rec.Code != tt.wantStatus {
   204  			t.Errorf("for request %q, status = %d; want %d", tt.req, rec.Code, tt.wantStatus)
   205  		}
   206  	}
   207  }
   208  
   209  var fsRedirectTestData = []struct {
   210  	original, redirect string
   211  }{
   212  	{"/test/index.html", "/test/"},
   213  	{"/test/testdata", "/test/testdata/"},
   214  	{"/test/testdata/file/", "/test/testdata/file"},
   215  }
   216  
   217  func TestFSRedirect(t *testing.T) {
   218  	defer afterTest(t)
   219  	ts := httptest.NewServer(StripPrefix("/test", FileServer(Dir("."))))
   220  	defer ts.Close()
   221  
   222  	for _, data := range fsRedirectTestData {
   223  		res, err := Get(ts.URL + data.original)
   224  		if err != nil {
   225  			t.Fatal(err)
   226  		}
   227  		res.Body.Close()
   228  		if g, e := res.Request.URL.Path, data.redirect; g != e {
   229  			t.Errorf("redirect from %s: got %s, want %s", data.original, g, e)
   230  		}
   231  	}
   232  }
   233  
   234  type testFileSystem struct {
   235  	open func(name string) (File, error)
   236  }
   237  
   238  func (fs *testFileSystem) Open(name string) (File, error) {
   239  	return fs.open(name)
   240  }
   241  
   242  func TestFileServerCleans(t *testing.T) {
   243  	defer afterTest(t)
   244  	ch := make(chan string, 1)
   245  	fs := FileServer(&testFileSystem{func(name string) (File, error) {
   246  		ch <- name
   247  		return nil, errors.New("file does not exist")
   248  	}})
   249  	tests := []struct {
   250  		reqPath, openArg string
   251  	}{
   252  		{"/foo.txt", "/foo.txt"},
   253  		{"//foo.txt", "/foo.txt"},
   254  		{"/../foo.txt", "/foo.txt"},
   255  	}
   256  	req, _ := NewRequest("GET", "http://example.com", nil)
   257  	for n, test := range tests {
   258  		rec := httptest.NewRecorder()
   259  		req.URL.Path = test.reqPath
   260  		fs.ServeHTTP(rec, req)
   261  		if got := <-ch; got != test.openArg {
   262  			t.Errorf("test %d: got %q, want %q", n, got, test.openArg)
   263  		}
   264  	}
   265  }
   266  
   267  func TestFileServerEscapesNames(t *testing.T) {
   268  	defer afterTest(t)
   269  	const dirListPrefix = "<pre>\n"
   270  	const dirListSuffix = "\n</pre>\n"
   271  	tests := []struct {
   272  		name, escaped string
   273  	}{
   274  		{`simple_name`, `<a href="simple_name">simple_name</a>`},
   275  		{`"'<>&`, `<a href="%22%27%3C%3E&">&#34;&#39;&lt;&gt;&amp;</a>`},
   276  		{`?foo=bar#baz`, `<a href="%3Ffoo=bar%23baz">?foo=bar#baz</a>`},
   277  		{`<combo>?foo`, `<a href="%3Ccombo%3E%3Ffoo">&lt;combo&gt;?foo</a>`},
   278  		{`foo:bar`, `<a href="./foo:bar">foo:bar</a>`},
   279  	}
   280  
   281  	// We put each test file in its own directory in the fakeFS so we can look at it in isolation.
   282  	fs := make(fakeFS)
   283  	for i, test := range tests {
   284  		testFile := &fakeFileInfo{basename: test.name}
   285  		fs[fmt.Sprintf("/%d", i)] = &fakeFileInfo{
   286  			dir:     true,
   287  			modtime: time.Unix(1000000000, 0).UTC(),
   288  			ents:    []*fakeFileInfo{testFile},
   289  		}
   290  		fs[fmt.Sprintf("/%d/%s", i, test.name)] = testFile
   291  	}
   292  
   293  	ts := httptest.NewServer(FileServer(&fs))
   294  	defer ts.Close()
   295  	for i, test := range tests {
   296  		url := fmt.Sprintf("%s/%d", ts.URL, i)
   297  		res, err := Get(url)
   298  		if err != nil {
   299  			t.Fatalf("test %q: Get: %v", test.name, err)
   300  		}
   301  		b, err := ioutil.ReadAll(res.Body)
   302  		if err != nil {
   303  			t.Fatalf("test %q: read Body: %v", test.name, err)
   304  		}
   305  		s := string(b)
   306  		if !strings.HasPrefix(s, dirListPrefix) || !strings.HasSuffix(s, dirListSuffix) {
   307  			t.Errorf("test %q: listing dir, full output is %q, want prefix %q and suffix %q", test.name, s, dirListPrefix, dirListSuffix)
   308  		}
   309  		if trimmed := strings.TrimSuffix(strings.TrimPrefix(s, dirListPrefix), dirListSuffix); trimmed != test.escaped {
   310  			t.Errorf("test %q: listing dir, filename escaped to %q, want %q", test.name, trimmed, test.escaped)
   311  		}
   312  		res.Body.Close()
   313  	}
   314  }
   315  
   316  func TestFileServerSortsNames(t *testing.T) {
   317  	defer afterTest(t)
   318  	const contents = "I am a fake file"
   319  	dirMod := time.Unix(123, 0).UTC()
   320  	fileMod := time.Unix(1000000000, 0).UTC()
   321  	fs := fakeFS{
   322  		"/": &fakeFileInfo{
   323  			dir:     true,
   324  			modtime: dirMod,
   325  			ents: []*fakeFileInfo{
   326  				{
   327  					basename: "b",
   328  					modtime:  fileMod,
   329  					contents: contents,
   330  				},
   331  				{
   332  					basename: "a",
   333  					modtime:  fileMod,
   334  					contents: contents,
   335  				},
   336  			},
   337  		},
   338  	}
   339  
   340  	ts := httptest.NewServer(FileServer(&fs))
   341  	defer ts.Close()
   342  
   343  	res, err := Get(ts.URL)
   344  	if err != nil {
   345  		t.Fatalf("Get: %v", err)
   346  	}
   347  	defer res.Body.Close()
   348  
   349  	b, err := ioutil.ReadAll(res.Body)
   350  	if err != nil {
   351  		t.Fatalf("read Body: %v", err)
   352  	}
   353  	s := string(b)
   354  	if !strings.Contains(s, "<a href=\"a\">a</a>\n<a href=\"b\">b</a>") {
   355  		t.Errorf("output appears to be unsorted:\n%s", s)
   356  	}
   357  }
   358  
   359  func mustRemoveAll(dir string) {
   360  	err := os.RemoveAll(dir)
   361  	if err != nil {
   362  		panic(err)
   363  	}
   364  }
   365  
   366  func TestFileServerImplicitLeadingSlash(t *testing.T) {
   367  	defer afterTest(t)
   368  	tempDir, err := ioutil.TempDir("", "")
   369  	if err != nil {
   370  		t.Fatalf("TempDir: %v", err)
   371  	}
   372  	defer mustRemoveAll(tempDir)
   373  	if err := ioutil.WriteFile(filepath.Join(tempDir, "foo.txt"), []byte("Hello world"), 0644); err != nil {
   374  		t.Fatalf("WriteFile: %v", err)
   375  	}
   376  	ts := httptest.NewServer(StripPrefix("/bar/", FileServer(Dir(tempDir))))
   377  	defer ts.Close()
   378  	get := func(suffix string) string {
   379  		res, err := Get(ts.URL + suffix)
   380  		if err != nil {
   381  			t.Fatalf("Get %s: %v", suffix, err)
   382  		}
   383  		b, err := ioutil.ReadAll(res.Body)
   384  		if err != nil {
   385  			t.Fatalf("ReadAll %s: %v", suffix, err)
   386  		}
   387  		res.Body.Close()
   388  		return string(b)
   389  	}
   390  	if s := get("/bar/"); !strings.Contains(s, ">foo.txt<") {
   391  		t.Logf("expected a directory listing with foo.txt, got %q", s)
   392  	}
   393  	if s := get("/bar/foo.txt"); s != "Hello world" {
   394  		t.Logf("expected %q, got %q", "Hello world", s)
   395  	}
   396  }
   397  
   398  func TestDirJoin(t *testing.T) {
   399  	if runtime.GOOS == "windows" {
   400  		t.Skip("skipping test on windows")
   401  	}
   402  	wfi, err := os.Stat("/etc/hosts")
   403  	if err != nil {
   404  		t.Skip("skipping test; no /etc/hosts file")
   405  	}
   406  	test := func(d Dir, name string) {
   407  		f, err := d.Open(name)
   408  		if err != nil {
   409  			t.Fatalf("open of %s: %v", name, err)
   410  		}
   411  		defer f.Close()
   412  		gfi, err := f.Stat()
   413  		if err != nil {
   414  			t.Fatalf("stat of %s: %v", name, err)
   415  		}
   416  		if !os.SameFile(gfi, wfi) {
   417  			t.Errorf("%s got different file", name)
   418  		}
   419  	}
   420  	test(Dir("/etc/"), "/hosts")
   421  	test(Dir("/etc/"), "hosts")
   422  	test(Dir("/etc/"), "../../../../hosts")
   423  	test(Dir("/etc"), "/hosts")
   424  	test(Dir("/etc"), "hosts")
   425  	test(Dir("/etc"), "../../../../hosts")
   426  
   427  	// Not really directories, but since we use this trick in
   428  	// ServeFile, test it:
   429  	test(Dir("/etc/hosts"), "")
   430  	test(Dir("/etc/hosts"), "/")
   431  	test(Dir("/etc/hosts"), "../")
   432  }
   433  
   434  func TestEmptyDirOpenCWD(t *testing.T) {
   435  	test := func(d Dir) {
   436  		name := "fs_test.go"
   437  		f, err := d.Open(name)
   438  		if err != nil {
   439  			t.Fatalf("open of %s: %v", name, err)
   440  		}
   441  		defer f.Close()
   442  	}
   443  	test(Dir(""))
   444  	test(Dir("."))
   445  	test(Dir("./"))
   446  }
   447  
   448  func TestServeFileContentType(t *testing.T) {
   449  	defer afterTest(t)
   450  	const ctype = "icecream/chocolate"
   451  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   452  		switch r.FormValue("override") {
   453  		case "1":
   454  			w.Header().Set("Content-Type", ctype)
   455  		case "2":
   456  			// Explicitly inhibit sniffing.
   457  			w.Header()["Content-Type"] = []string{}
   458  		}
   459  		ServeFile(w, r, "testdata/file")
   460  	}))
   461  	defer ts.Close()
   462  	get := func(override string, want []string) {
   463  		resp, err := Get(ts.URL + "?override=" + override)
   464  		if err != nil {
   465  			t.Fatal(err)
   466  		}
   467  		if h := resp.Header["Content-Type"]; !reflect.DeepEqual(h, want) {
   468  			t.Errorf("Content-Type mismatch: got %v, want %v", h, want)
   469  		}
   470  		resp.Body.Close()
   471  	}
   472  	get("0", []string{"text/plain; charset=utf-8"})
   473  	get("1", []string{ctype})
   474  	get("2", nil)
   475  }
   476  
   477  func TestServeFileMimeType(t *testing.T) {
   478  	defer afterTest(t)
   479  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   480  		ServeFile(w, r, "testdata/style.css")
   481  	}))
   482  	defer ts.Close()
   483  	resp, err := Get(ts.URL)
   484  	if err != nil {
   485  		t.Fatal(err)
   486  	}
   487  	resp.Body.Close()
   488  	want := "text/css; charset=utf-8"
   489  	if h := resp.Header.Get("Content-Type"); h != want {
   490  		t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
   491  	}
   492  }
   493  
   494  func TestServeFileFromCWD(t *testing.T) {
   495  	defer afterTest(t)
   496  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   497  		ServeFile(w, r, "fs_test.go")
   498  	}))
   499  	defer ts.Close()
   500  	r, err := Get(ts.URL)
   501  	if err != nil {
   502  		t.Fatal(err)
   503  	}
   504  	r.Body.Close()
   505  	if r.StatusCode != 200 {
   506  		t.Fatalf("expected 200 OK, got %s", r.Status)
   507  	}
   508  }
   509  
   510  // Issue 13996
   511  func TestServeDirWithoutTrailingSlash(t *testing.T) {
   512  	e := "/testdata/"
   513  	defer afterTest(t)
   514  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   515  		ServeFile(w, r, ".")
   516  	}))
   517  	defer ts.Close()
   518  	r, err := Get(ts.URL + "/testdata")
   519  	if err != nil {
   520  		t.Fatal(err)
   521  	}
   522  	r.Body.Close()
   523  	if g := r.Request.URL.Path; g != e {
   524  		t.Errorf("got %s, want %s", g, e)
   525  	}
   526  }
   527  
   528  // Tests that ServeFile doesn't add a Content-Length if a Content-Encoding is
   529  // specified.
   530  func TestServeFileWithContentEncoding_h1(t *testing.T) { testServeFileWithContentEncoding(t, h1Mode) }
   531  func TestServeFileWithContentEncoding_h2(t *testing.T) { testServeFileWithContentEncoding(t, h2Mode) }
   532  func testServeFileWithContentEncoding(t *testing.T, h2 bool) {
   533  	defer afterTest(t)
   534  	cst := newClientServerTest(t, h2, HandlerFunc(func(w ResponseWriter, r *Request) {
   535  		w.Header().Set("Content-Encoding", "foo")
   536  		ServeFile(w, r, "testdata/file")
   537  
   538  		// Because the testdata is so small, it would fit in
   539  		// both the h1 and h2 Server's write buffers. For h1,
   540  		// sendfile is used, though, forcing a header flush at
   541  		// the io.Copy. http2 doesn't do a header flush so
   542  		// buffers all 11 bytes and then adds its own
   543  		// Content-Length. To prevent the Server's
   544  		// Content-Length and test ServeFile only, flush here.
   545  		w.(Flusher).Flush()
   546  	}))
   547  	defer cst.close()
   548  	resp, err := cst.c.Get(cst.ts.URL)
   549  	if err != nil {
   550  		t.Fatal(err)
   551  	}
   552  	resp.Body.Close()
   553  	if g, e := resp.ContentLength, int64(-1); g != e {
   554  		t.Errorf("Content-Length mismatch: got %d, want %d", g, e)
   555  	}
   556  }
   557  
   558  func TestServeIndexHtml(t *testing.T) {
   559  	defer afterTest(t)
   560  	const want = "index.html says hello\n"
   561  	ts := httptest.NewServer(FileServer(Dir(".")))
   562  	defer ts.Close()
   563  
   564  	for _, path := range []string{"/testdata/", "/testdata/index.html"} {
   565  		res, err := Get(ts.URL + path)
   566  		if err != nil {
   567  			t.Fatal(err)
   568  		}
   569  		b, err := ioutil.ReadAll(res.Body)
   570  		if err != nil {
   571  			t.Fatal("reading Body:", err)
   572  		}
   573  		if s := string(b); s != want {
   574  			t.Errorf("for path %q got %q, want %q", path, s, want)
   575  		}
   576  		res.Body.Close()
   577  	}
   578  }
   579  
   580  func TestFileServerZeroByte(t *testing.T) {
   581  	defer afterTest(t)
   582  	ts := httptest.NewServer(FileServer(Dir(".")))
   583  	defer ts.Close()
   584  
   585  	res, err := Get(ts.URL + "/..\x00")
   586  	if err != nil {
   587  		t.Fatal(err)
   588  	}
   589  	b, err := ioutil.ReadAll(res.Body)
   590  	if err != nil {
   591  		t.Fatal("reading Body:", err)
   592  	}
   593  	if res.StatusCode == 200 {
   594  		t.Errorf("got status 200; want an error. Body is:\n%s", string(b))
   595  	}
   596  }
   597  
   598  type fakeFileInfo struct {
   599  	dir      bool
   600  	basename string
   601  	modtime  time.Time
   602  	ents     []*fakeFileInfo
   603  	contents string
   604  	err      error
   605  }
   606  
   607  func (f *fakeFileInfo) Name() string       { return f.basename }
   608  func (f *fakeFileInfo) Sys() interface{}   { return nil }
   609  func (f *fakeFileInfo) ModTime() time.Time { return f.modtime }
   610  func (f *fakeFileInfo) IsDir() bool        { return f.dir }
   611  func (f *fakeFileInfo) Size() int64        { return int64(len(f.contents)) }
   612  func (f *fakeFileInfo) Mode() os.FileMode {
   613  	if f.dir {
   614  		return 0755 | os.ModeDir
   615  	}
   616  	return 0644
   617  }
   618  
   619  type fakeFile struct {
   620  	io.ReadSeeker
   621  	fi     *fakeFileInfo
   622  	path   string // as opened
   623  	entpos int
   624  }
   625  
   626  func (f *fakeFile) Close() error               { return nil }
   627  func (f *fakeFile) Stat() (os.FileInfo, error) { return f.fi, nil }
   628  func (f *fakeFile) Readdir(count int) ([]os.FileInfo, error) {
   629  	if !f.fi.dir {
   630  		return nil, os.ErrInvalid
   631  	}
   632  	var fis []os.FileInfo
   633  
   634  	limit := f.entpos + count
   635  	if count <= 0 || limit > len(f.fi.ents) {
   636  		limit = len(f.fi.ents)
   637  	}
   638  	for ; f.entpos < limit; f.entpos++ {
   639  		fis = append(fis, f.fi.ents[f.entpos])
   640  	}
   641  
   642  	if len(fis) == 0 && count > 0 {
   643  		return fis, io.EOF
   644  	} else {
   645  		return fis, nil
   646  	}
   647  }
   648  
   649  type fakeFS map[string]*fakeFileInfo
   650  
   651  func (fs fakeFS) Open(name string) (File, error) {
   652  	name = path.Clean(name)
   653  	f, ok := fs[name]
   654  	if !ok {
   655  		return nil, os.ErrNotExist
   656  	}
   657  	if f.err != nil {
   658  		return nil, f.err
   659  	}
   660  	return &fakeFile{ReadSeeker: strings.NewReader(f.contents), fi: f, path: name}, nil
   661  }
   662  
   663  func TestDirectoryIfNotModified(t *testing.T) {
   664  	defer afterTest(t)
   665  	const indexContents = "I am a fake index.html file"
   666  	fileMod := time.Unix(1000000000, 0).UTC()
   667  	fileModStr := fileMod.Format(TimeFormat)
   668  	dirMod := time.Unix(123, 0).UTC()
   669  	indexFile := &fakeFileInfo{
   670  		basename: "index.html",
   671  		modtime:  fileMod,
   672  		contents: indexContents,
   673  	}
   674  	fs := fakeFS{
   675  		"/": &fakeFileInfo{
   676  			dir:     true,
   677  			modtime: dirMod,
   678  			ents:    []*fakeFileInfo{indexFile},
   679  		},
   680  		"/index.html": indexFile,
   681  	}
   682  
   683  	ts := httptest.NewServer(FileServer(fs))
   684  	defer ts.Close()
   685  
   686  	res, err := Get(ts.URL)
   687  	if err != nil {
   688  		t.Fatal(err)
   689  	}
   690  	b, err := ioutil.ReadAll(res.Body)
   691  	if err != nil {
   692  		t.Fatal(err)
   693  	}
   694  	if string(b) != indexContents {
   695  		t.Fatalf("Got body %q; want %q", b, indexContents)
   696  	}
   697  	res.Body.Close()
   698  
   699  	lastMod := res.Header.Get("Last-Modified")
   700  	if lastMod != fileModStr {
   701  		t.Fatalf("initial Last-Modified = %q; want %q", lastMod, fileModStr)
   702  	}
   703  
   704  	req, _ := NewRequest("GET", ts.URL, nil)
   705  	req.Header.Set("If-Modified-Since", lastMod)
   706  
   707  	res, err = DefaultClient.Do(req)
   708  	if err != nil {
   709  		t.Fatal(err)
   710  	}
   711  	if res.StatusCode != 304 {
   712  		t.Fatalf("Code after If-Modified-Since request = %v; want 304", res.StatusCode)
   713  	}
   714  	res.Body.Close()
   715  
   716  	// Advance the index.html file's modtime, but not the directory's.
   717  	indexFile.modtime = indexFile.modtime.Add(1 * time.Hour)
   718  
   719  	res, err = DefaultClient.Do(req)
   720  	if err != nil {
   721  		t.Fatal(err)
   722  	}
   723  	if res.StatusCode != 200 {
   724  		t.Fatalf("Code after second If-Modified-Since request = %v; want 200; res is %#v", res.StatusCode, res)
   725  	}
   726  	res.Body.Close()
   727  }
   728  
   729  func mustStat(t *testing.T, fileName string) os.FileInfo {
   730  	fi, err := os.Stat(fileName)
   731  	if err != nil {
   732  		t.Fatal(err)
   733  	}
   734  	return fi
   735  }
   736  
   737  func TestServeContent(t *testing.T) {
   738  	defer afterTest(t)
   739  	type serveParam struct {
   740  		name        string
   741  		modtime     time.Time
   742  		content     io.ReadSeeker
   743  		contentType string
   744  		etag        string
   745  	}
   746  	servec := make(chan serveParam, 1)
   747  	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
   748  		p := <-servec
   749  		if p.etag != "" {
   750  			w.Header().Set("ETag", p.etag)
   751  		}
   752  		if p.contentType != "" {
   753  			w.Header().Set("Content-Type", p.contentType)
   754  		}
   755  		ServeContent(w, r, p.name, p.modtime, p.content)
   756  	}))
   757  	defer ts.Close()
   758  
   759  	type testCase struct {
   760  		// One of file or content must be set:
   761  		file    string
   762  		content io.ReadSeeker
   763  
   764  		modtime          time.Time
   765  		serveETag        string // optional
   766  		serveContentType string // optional
   767  		reqHeader        map[string]string
   768  		wantLastMod      string
   769  		wantContentType  string
   770  		wantContentRange string
   771  		wantStatus       int
   772  	}
   773  	htmlModTime := mustStat(t, "testdata/index.html").ModTime()
   774  	tests := map[string]testCase{
   775  		"no_last_modified": {
   776  			file:            "testdata/style.css",
   777  			wantContentType: "text/css; charset=utf-8",
   778  			wantStatus:      200,
   779  		},
   780  		"with_last_modified": {
   781  			file:            "testdata/index.html",
   782  			wantContentType: "text/html; charset=utf-8",
   783  			modtime:         htmlModTime,
   784  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   785  			wantStatus:      200,
   786  		},
   787  		"not_modified_modtime": {
   788  			file:      "testdata/style.css",
   789  			serveETag: `"foo"`, // Last-Modified sent only when no ETag
   790  			modtime:   htmlModTime,
   791  			reqHeader: map[string]string{
   792  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   793  			},
   794  			wantStatus: 304,
   795  		},
   796  		"not_modified_modtime_with_contenttype": {
   797  			file:             "testdata/style.css",
   798  			serveContentType: "text/css", // explicit content type
   799  			serveETag:        `"foo"`,    // Last-Modified sent only when no ETag
   800  			modtime:          htmlModTime,
   801  			reqHeader: map[string]string{
   802  				"If-Modified-Since": htmlModTime.UTC().Format(TimeFormat),
   803  			},
   804  			wantStatus: 304,
   805  		},
   806  		"not_modified_etag": {
   807  			file:      "testdata/style.css",
   808  			serveETag: `"foo"`,
   809  			reqHeader: map[string]string{
   810  				"If-None-Match": `"foo"`,
   811  			},
   812  			wantStatus: 304,
   813  		},
   814  		"not_modified_etag_no_seek": {
   815  			content:   panicOnSeek{nil}, // should never be called
   816  			serveETag: `W/"foo"`,        // If-None-Match uses weak ETag comparison
   817  			reqHeader: map[string]string{
   818  				"If-None-Match": `"baz", W/"foo"`,
   819  			},
   820  			wantStatus: 304,
   821  		},
   822  		"if_none_match_mismatch": {
   823  			file:      "testdata/style.css",
   824  			serveETag: `"foo"`,
   825  			reqHeader: map[string]string{
   826  				"If-None-Match": `"Foo"`,
   827  			},
   828  			wantStatus:      200,
   829  			wantContentType: "text/css; charset=utf-8",
   830  		},
   831  		"range_good": {
   832  			file:      "testdata/style.css",
   833  			serveETag: `"A"`,
   834  			reqHeader: map[string]string{
   835  				"Range": "bytes=0-4",
   836  			},
   837  			wantStatus:       StatusPartialContent,
   838  			wantContentType:  "text/css; charset=utf-8",
   839  			wantContentRange: "bytes 0-4/8",
   840  		},
   841  		"range_match": {
   842  			file:      "testdata/style.css",
   843  			serveETag: `"A"`,
   844  			reqHeader: map[string]string{
   845  				"Range":    "bytes=0-4",
   846  				"If-Range": `"A"`,
   847  			},
   848  			wantStatus:       StatusPartialContent,
   849  			wantContentType:  "text/css; charset=utf-8",
   850  			wantContentRange: "bytes 0-4/8",
   851  		},
   852  		"range_match_weak_etag": {
   853  			file:      "testdata/style.css",
   854  			serveETag: `W/"A"`,
   855  			reqHeader: map[string]string{
   856  				"Range":    "bytes=0-4",
   857  				"If-Range": `W/"A"`,
   858  			},
   859  			wantStatus:      200,
   860  			wantContentType: "text/css; charset=utf-8",
   861  		},
   862  		"range_no_overlap": {
   863  			file:      "testdata/style.css",
   864  			serveETag: `"A"`,
   865  			reqHeader: map[string]string{
   866  				"Range": "bytes=10-20",
   867  			},
   868  			wantStatus:       StatusRequestedRangeNotSatisfiable,
   869  			wantContentType:  "text/plain; charset=utf-8",
   870  			wantContentRange: "bytes */8",
   871  		},
   872  		// An If-Range resource for entity "A", but entity "B" is now current.
   873  		// The Range request should be ignored.
   874  		"range_no_match": {
   875  			file:      "testdata/style.css",
   876  			serveETag: `"A"`,
   877  			reqHeader: map[string]string{
   878  				"Range":    "bytes=0-4",
   879  				"If-Range": `"B"`,
   880  			},
   881  			wantStatus:      200,
   882  			wantContentType: "text/css; charset=utf-8",
   883  		},
   884  		"range_with_modtime": {
   885  			file:    "testdata/style.css",
   886  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 0 /* nanos */, time.UTC),
   887  			reqHeader: map[string]string{
   888  				"Range":    "bytes=0-4",
   889  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
   890  			},
   891  			wantStatus:       StatusPartialContent,
   892  			wantContentType:  "text/css; charset=utf-8",
   893  			wantContentRange: "bytes 0-4/8",
   894  			wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
   895  		},
   896  		"range_with_modtime_nanos": {
   897  			file:    "testdata/style.css",
   898  			modtime: time.Date(2014, 6, 25, 17, 12, 18, 123 /* nanos */, time.UTC),
   899  			reqHeader: map[string]string{
   900  				"Range":    "bytes=0-4",
   901  				"If-Range": "Wed, 25 Jun 2014 17:12:18 GMT",
   902  			},
   903  			wantStatus:       StatusPartialContent,
   904  			wantContentType:  "text/css; charset=utf-8",
   905  			wantContentRange: "bytes 0-4/8",
   906  			wantLastMod:      "Wed, 25 Jun 2014 17:12:18 GMT",
   907  		},
   908  		"unix_zero_modtime": {
   909  			content:         strings.NewReader("<html>foo"),
   910  			modtime:         time.Unix(0, 0),
   911  			wantStatus:      StatusOK,
   912  			wantContentType: "text/html; charset=utf-8",
   913  		},
   914  		"ifmatch_matches": {
   915  			file:      "testdata/style.css",
   916  			serveETag: `"A"`,
   917  			reqHeader: map[string]string{
   918  				"If-Match": `"Z", "A"`,
   919  			},
   920  			wantStatus:      200,
   921  			wantContentType: "text/css; charset=utf-8",
   922  		},
   923  		"ifmatch_star": {
   924  			file:      "testdata/style.css",
   925  			serveETag: `"A"`,
   926  			reqHeader: map[string]string{
   927  				"If-Match": `*`,
   928  			},
   929  			wantStatus:      200,
   930  			wantContentType: "text/css; charset=utf-8",
   931  		},
   932  		"ifmatch_failed": {
   933  			file:      "testdata/style.css",
   934  			serveETag: `"A"`,
   935  			reqHeader: map[string]string{
   936  				"If-Match": `"B"`,
   937  			},
   938  			wantStatus:      412,
   939  			wantContentType: "text/plain; charset=utf-8",
   940  		},
   941  		"ifmatch_fails_on_weak_etag": {
   942  			file:      "testdata/style.css",
   943  			serveETag: `W/"A"`,
   944  			reqHeader: map[string]string{
   945  				"If-Match": `W/"A"`,
   946  			},
   947  			wantStatus:      412,
   948  			wantContentType: "text/plain; charset=utf-8",
   949  		},
   950  		"if_unmodified_since_true": {
   951  			file:    "testdata/style.css",
   952  			modtime: htmlModTime,
   953  			reqHeader: map[string]string{
   954  				"If-Unmodified-Since": htmlModTime.UTC().Format(TimeFormat),
   955  			},
   956  			wantStatus:      200,
   957  			wantContentType: "text/css; charset=utf-8",
   958  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   959  		},
   960  		"if_unmodified_since_false": {
   961  			file:    "testdata/style.css",
   962  			modtime: htmlModTime,
   963  			reqHeader: map[string]string{
   964  				"If-Unmodified-Since": htmlModTime.Add(-2 * time.Second).UTC().Format(TimeFormat),
   965  			},
   966  			wantStatus:      412,
   967  			wantContentType: "text/plain; charset=utf-8",
   968  			wantLastMod:     htmlModTime.UTC().Format(TimeFormat),
   969  		},
   970  	}
   971  	for testName, tt := range tests {
   972  		var content io.ReadSeeker
   973  		if tt.file != "" {
   974  			f, err := os.Open(tt.file)
   975  			if err != nil {
   976  				t.Fatalf("test %q: %v", testName, err)
   977  			}
   978  			defer f.Close()
   979  			content = f
   980  		} else {
   981  			content = tt.content
   982  		}
   983  
   984  		servec <- serveParam{
   985  			name:        filepath.Base(tt.file),
   986  			content:     content,
   987  			modtime:     tt.modtime,
   988  			etag:        tt.serveETag,
   989  			contentType: tt.serveContentType,
   990  		}
   991  		req, err := NewRequest("GET", ts.URL, nil)
   992  		if err != nil {
   993  			t.Fatal(err)
   994  		}
   995  		for k, v := range tt.reqHeader {
   996  			req.Header.Set(k, v)
   997  		}
   998  		res, err := DefaultClient.Do(req)
   999  		if err != nil {
  1000  			t.Fatal(err)
  1001  		}
  1002  		io.Copy(ioutil.Discard, res.Body)
  1003  		res.Body.Close()
  1004  		if res.StatusCode != tt.wantStatus {
  1005  			t.Errorf("test %q: status = %d; want %d", testName, res.StatusCode, tt.wantStatus)
  1006  		}
  1007  		if g, e := res.Header.Get("Content-Type"), tt.wantContentType; g != e {
  1008  			t.Errorf("test %q: content-type = %q, want %q", testName, g, e)
  1009  		}
  1010  		if g, e := res.Header.Get("Content-Range"), tt.wantContentRange; g != e {
  1011  			t.Errorf("test %q: content-range = %q, want %q", testName, g, e)
  1012  		}
  1013  		if g, e := res.Header.Get("Last-Modified"), tt.wantLastMod; g != e {
  1014  			t.Errorf("test %q: last-modified = %q, want %q", testName, g, e)
  1015  		}
  1016  	}
  1017  }
  1018  
  1019  // Issue 12991
  1020  func TestServerFileStatError(t *testing.T) {
  1021  	rec := httptest.NewRecorder()
  1022  	r, _ := NewRequest("GET", "http://foo/", nil)
  1023  	redirect := false
  1024  	name := "file.txt"
  1025  	fs := issue12991FS{}
  1026  	ExportServeFile(rec, r, fs, name, redirect)
  1027  	if body := rec.Body.String(); !strings.Contains(body, "403") || !strings.Contains(body, "Forbidden") {
  1028  		t.Errorf("wanted 403 forbidden message; got: %s", body)
  1029  	}
  1030  }
  1031  
  1032  type issue12991FS struct{}
  1033  
  1034  func (issue12991FS) Open(string) (File, error) { return issue12991File{}, nil }
  1035  
  1036  type issue12991File struct{ File }
  1037  
  1038  func (issue12991File) Stat() (os.FileInfo, error) { return nil, os.ErrPermission }
  1039  func (issue12991File) Close() error               { return nil }
  1040  
  1041  func TestServeContentErrorMessages(t *testing.T) {
  1042  	defer afterTest(t)
  1043  	fs := fakeFS{
  1044  		"/500": &fakeFileInfo{
  1045  			err: errors.New("random error"),
  1046  		},
  1047  		"/403": &fakeFileInfo{
  1048  			err: &os.PathError{Err: os.ErrPermission},
  1049  		},
  1050  	}
  1051  	ts := httptest.NewServer(FileServer(fs))
  1052  	defer ts.Close()
  1053  	for _, code := range []int{403, 404, 500} {
  1054  		res, err := DefaultClient.Get(fmt.Sprintf("%s/%d", ts.URL, code))
  1055  		if err != nil {
  1056  			t.Errorf("Error fetching /%d: %v", code, err)
  1057  			continue
  1058  		}
  1059  		if res.StatusCode != code {
  1060  			t.Errorf("For /%d, status code = %d; want %d", code, res.StatusCode, code)
  1061  		}
  1062  		res.Body.Close()
  1063  	}
  1064  }
  1065  
  1066  // verifies that sendfile is being used on Linux
  1067  func TestLinuxSendfile(t *testing.T) {
  1068  	setParallel(t)
  1069  	defer afterTest(t)
  1070  	if runtime.GOOS != "linux" {
  1071  		t.Skip("skipping; linux-only test")
  1072  	}
  1073  	if _, err := exec.LookPath("strace"); err != nil {
  1074  		t.Skip("skipping; strace not found in path")
  1075  	}
  1076  
  1077  	ln, err := net.Listen("tcp", "127.0.0.1:0")
  1078  	if err != nil {
  1079  		t.Fatal(err)
  1080  	}
  1081  	lnf, err := ln.(*net.TCPListener).File()
  1082  	if err != nil {
  1083  		t.Fatal(err)
  1084  	}
  1085  	defer ln.Close()
  1086  
  1087  	syscalls := "sendfile,sendfile64"
  1088  	switch runtime.GOARCH {
  1089  	case "mips64le", "s390x":
  1090  		// strace on the above platforms doesn't support sendfile64
  1091  		// and will error out if we specify that with `-e trace='.
  1092  		syscalls = "sendfile"
  1093  	case "mips64":
  1094  		t.Skip("TODO: update this test to be robust against various versions of strace on mips64. See golang.org/issue/33430")
  1095  	}
  1096  
  1097  	var buf bytes.Buffer
  1098  	child := exec.Command("strace", "-f", "-q", "-e", "trace="+syscalls, os.Args[0], "-test.run=TestLinuxSendfileChild")
  1099  	child.ExtraFiles = append(child.ExtraFiles, lnf)
  1100  	child.Env = append([]string{"GO_WANT_HELPER_PROCESS=1"}, os.Environ()...)
  1101  	child.Stdout = &buf
  1102  	child.Stderr = &buf
  1103  	if err := child.Start(); err != nil {
  1104  		t.Skipf("skipping; failed to start straced child: %v", err)
  1105  	}
  1106  
  1107  	res, err := Get(fmt.Sprintf("http://%s/", ln.Addr()))
  1108  	if err != nil {
  1109  		t.Fatalf("http client error: %v", err)
  1110  	}
  1111  	_, err = io.Copy(ioutil.Discard, res.Body)
  1112  	if err != nil {
  1113  		t.Fatalf("client body read error: %v", err)
  1114  	}
  1115  	res.Body.Close()
  1116  
  1117  	// Force child to exit cleanly.
  1118  	Post(fmt.Sprintf("http://%s/quit", ln.Addr()), "", nil)
  1119  	child.Wait()
  1120  
  1121  	rx := regexp.MustCompile(`sendfile(64)?\(\d+,\s*\d+,\s*NULL,\s*\d+`)
  1122  	out := buf.String()
  1123  	if !rx.MatchString(out) {
  1124  		t.Errorf("no sendfile system call found in:\n%s", out)
  1125  	}
  1126  }
  1127  
  1128  func getBody(t *testing.T, testName string, req Request) (*Response, []byte) {
  1129  	r, err := DefaultClient.Do(&req)
  1130  	if err != nil {
  1131  		t.Fatalf("%s: for URL %q, send error: %v", testName, req.URL.String(), err)
  1132  	}
  1133  	b, err := ioutil.ReadAll(r.Body)
  1134  	if err != nil {
  1135  		t.Fatalf("%s: for URL %q, reading body: %v", testName, req.URL.String(), err)
  1136  	}
  1137  	return r, b
  1138  }
  1139  
  1140  // TestLinuxSendfileChild isn't a real test. It's used as a helper process
  1141  // for TestLinuxSendfile.
  1142  func TestLinuxSendfileChild(*testing.T) {
  1143  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
  1144  		return
  1145  	}
  1146  	defer os.Exit(0)
  1147  	fd3 := os.NewFile(3, "ephemeral-port-listener")
  1148  	ln, err := net.FileListener(fd3)
  1149  	if err != nil {
  1150  		panic(err)
  1151  	}
  1152  	mux := NewServeMux()
  1153  	mux.Handle("/", FileServer(Dir("testdata")))
  1154  	mux.HandleFunc("/quit", func(ResponseWriter, *Request) {
  1155  		os.Exit(0)
  1156  	})
  1157  	s := &Server{Handler: mux}
  1158  	err = s.Serve(ln)
  1159  	if err != nil {
  1160  		panic(err)
  1161  	}
  1162  }
  1163  
  1164  func TestFileServerCleanPath(t *testing.T) {
  1165  	tests := []struct {
  1166  		path     string
  1167  		wantCode int
  1168  		wantOpen []string
  1169  	}{
  1170  		{"/", 200, []string{"/", "/index.html"}},
  1171  		{"/dir", 301, []string{"/dir"}},
  1172  		{"/dir/", 200, []string{"/dir", "/dir/index.html"}},
  1173  	}
  1174  	for _, tt := range tests {
  1175  		var log []string
  1176  		rr := httptest.NewRecorder()
  1177  		req, _ := NewRequest("GET", "http://foo.localhost"+tt.path, nil)
  1178  		FileServer(fileServerCleanPathDir{&log}).ServeHTTP(rr, req)
  1179  		if !reflect.DeepEqual(log, tt.wantOpen) {
  1180  			t.Logf("For %s: Opens = %q; want %q", tt.path, log, tt.wantOpen)
  1181  		}
  1182  		if rr.Code != tt.wantCode {
  1183  			t.Logf("For %s: Response code = %d; want %d", tt.path, rr.Code, tt.wantCode)
  1184  		}
  1185  	}
  1186  }
  1187  
  1188  type fileServerCleanPathDir struct {
  1189  	log *[]string
  1190  }
  1191  
  1192  func (d fileServerCleanPathDir) Open(path string) (File, error) {
  1193  	*(d.log) = append(*(d.log), path)
  1194  	if path == "/" || path == "/dir" || path == "/dir/" {
  1195  		// Just return back something that's a directory.
  1196  		return Dir(".").Open(".")
  1197  	}
  1198  	return nil, os.ErrNotExist
  1199  }
  1200  
  1201  type panicOnSeek struct{ io.ReadSeeker }
  1202  
  1203  func Test_scanETag(t *testing.T) {
  1204  	tests := []struct {
  1205  		in         string
  1206  		wantETag   string
  1207  		wantRemain string
  1208  	}{
  1209  		{`W/"etag-1"`, `W/"etag-1"`, ""},
  1210  		{`"etag-2"`, `"etag-2"`, ""},
  1211  		{`"etag-1", "etag-2"`, `"etag-1"`, `, "etag-2"`},
  1212  		{"", "", ""},
  1213  		{"", "", ""},
  1214  		{"W/", "", ""},
  1215  		{`W/"truc`, "", ""},
  1216  		{`w/"case-sensitive"`, "", ""},
  1217  	}
  1218  	for _, test := range tests {
  1219  		etag, remain := ExportScanETag(test.in)
  1220  		if etag != test.wantETag || remain != test.wantRemain {
  1221  			t.Errorf("scanETag(%q)=%q %q, want %q %q", test.in, etag, remain, test.wantETag, test.wantRemain)
  1222  		}
  1223  	}
  1224  }