github.com/pzeinlinger/servefiles@v3.1.0+incompatible/assets_test.go (about)

     1  // MIT License
     2  //
     3  // Copyright (c) 2016 Rick Beton
     4  //
     5  // Permission is hereby granted, free of charge, to any person obtaining a copy
     6  // of this software and associated documentation files (the "Software"), to deal
     7  // in the Software without restriction, including without limitation the rights
     8  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     9  // copies of the Software, and to permit persons to whom the Software is
    10  // furnished to do so, subject to the following conditions:
    11  //
    12  // The above copyright notice and this permission notice shall be included in all
    13  // copies or substantial portions of the Software.
    14  //
    15  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    16  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    17  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    18  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    19  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    20  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    21  // SOFTWARE.
    22  
    23  package servefiles
    24  
    25  import (
    26  	"fmt"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	. "net/url"
    30  	"os"
    31  	"reflect"
    32  	"strings"
    33  	"testing"
    34  	"time"
    35  	"github.com/spf13/afero"
    36  )
    37  
    38  var emptyStrings []string
    39  
    40  func mustChdir(dir string) {
    41  	err := os.Chdir(dir)
    42  	if err != nil {
    43  		panic(err)
    44  	}
    45  }
    46  
    47  func init() {
    48  	mustChdir("test")
    49  }
    50  
    51  func TestChooseResourceSimpleNoGzip(t *testing.T) {
    52  	cases := []struct {
    53  		n                       int
    54  		maxAge                  time.Duration
    55  		url, path, cacheControl string
    56  	}{
    57  		{0, 1, "http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=1"},
    58  		{0, 3671, "http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=3671"},
    59  		{3, 3671, "http://localhost:8001/x/y/z/img/sort_asc.png", "assets/img/sort_asc.png", "public, maxAge=3671"},
    60  	}
    61  
    62  	for i, test := range cases {
    63  		etag := etagFor(test.path)
    64  		url := mustUrl(test.url)
    65  		request := &http.Request{Method: "GET", URL: url}
    66  		a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second)
    67  		w := httptest.NewRecorder()
    68  
    69  		a.ServeHTTP(w, request)
    70  
    71  		isEqual(t, w.Code, 200, i)
    72  		//isEqual(t, message, "", test.path)
    73  		isEqual(t, len(w.Header()["Expires"]), 1, i)
    74  		isGte(t, len(w.Header()["Expires"][0]), 25, i)
    75  		//fmt.Println(headers["Expires"])
    76  		isEqual(t, w.Header()["Cache-Control"], []string{test.cacheControl}, i)
    77  		isEqual(t, w.Header()["Etag"], []string{etag}, i)
    78  		isEqual(t, w.Body.Len(), 160, i)
    79  	}
    80  }
    81  
    82  func TestChooseResourceSimpleNonExistent(t *testing.T) {
    83  	cases := []struct {
    84  		n      int
    85  		maxAge time.Duration
    86  		url    string
    87  	}{
    88  		{0, time.Second, "http://localhost:8001/img/nonexisting.png"},
    89  		{1, time.Second, "http://localhost:8001/a/img/nonexisting.png"},
    90  		{2, time.Second, "http://localhost:8001/a/b/img/nonexisting.png"},
    91  	}
    92  
    93  	for i, test := range cases {
    94  		url := mustUrl(test.url)
    95  		request := &http.Request{Method: "GET", URL: url}
    96  		a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge)
    97  		w := httptest.NewRecorder()
    98  
    99  		a.ServeHTTP(w, request)
   100  
   101  		isEqual(t, w.Code, 404, i)
   102  		//t.Logf("header %v", w.Header())
   103  		isGte(t, len(w.Header()), 4, i)
   104  		isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i)
   105  		isEqual(t, w.Header().Get("Cache-Control"), "public, maxAge=1", i)
   106  		isGte(t, len(w.Header().Get("Expires")), 25, i)
   107  	}
   108  }
   109  
   110  func TestServeHTTP200WithGzipAndGzipWithAcceptHeader(t *testing.T) {
   111  	cases := []struct {
   112  		n                             int
   113  		maxAge                        time.Duration
   114  		url, mime, path, cacheControl string
   115  	}{
   116  		{0, 1, "http://localhost:8001/css/style1.css", "text/css; charset=utf-8", "assets/css/style1.css.gz", "public, maxAge=1"},
   117  		{2, 1, "http://localhost:8001/a/b/css/style1.css", "text/css; charset=utf-8", "assets/css/style1.css.gz", "public, maxAge=1"},
   118  		{0, 1, "http://localhost:8001/js/script1.js", "application/javascript", "assets/js/script1.js.gz", "public, maxAge=1"},
   119  		{2, 1, "http://localhost:8001/a/b/js/script1.js", "application/javascript", "assets/js/script1.js.gz", "public, maxAge=1"},
   120  	}
   121  
   122  	for _, test := range cases {
   123  		etag := etagFor(test.path)
   124  		url := mustUrl(test.url)
   125  		header := newHeader("Accept-Encoding", "xxxx, gzip, zzz")
   126  		request := &http.Request{Method: "GET", URL: url, Header: header}
   127  		a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second)
   128  		w := httptest.NewRecorder()
   129  
   130  		a.ServeHTTP(w, request)
   131  
   132  		isEqual(t, w.Code, 200, test.path)
   133  		headers := w.Header()
   134  		//t.Logf("%+v\n", headers)
   135  		isGte(t, len(headers), 7, test.path)
   136  		isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path)
   137  		isEqual(t, headers["Content-Type"], []string{test.mime}, test.path)
   138  		isEqual(t, headers["X-Content-Type-Options"], []string{"nosniff"}, test.path)
   139  		isEqual(t, headers["Content-Encoding"], []string{"gzip"}, test.path)
   140  		isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, test.path)
   141  		isEqual(t, headers["Etag"], []string{etag}, test.path)
   142  		isEqual(t, len(headers["Expires"]), 1, test.path)
   143  		isGte(t, len(headers["Expires"][0]), 25, test.path)
   144  	}
   145  }
   146  
   147  func TestServeHTTP200WithGzipButNoAcceptHeader(t *testing.T) {
   148  	cases := []struct {
   149  		n                             int
   150  		maxAge                        time.Duration
   151  		url, mime, path, cacheControl string
   152  	}{
   153  		{0, 1, "http://localhost:8001/css/style1.css", "text/css; charset=utf-8", "assets/css/style1.css", "public, maxAge=1"},
   154  		{2, 2, "http://localhost:8001/a/b/css/style1.css", "text/css; charset=utf-8", "assets/css/style1.css", "public, maxAge=2"},
   155  		{0, 3, "http://localhost:8001/js/script1.js", "application/javascript", "assets/js/script1.js", "public, maxAge=3"},
   156  		{2, 4, "http://localhost:8001/a/b/js/script1.js", "application/javascript", "assets/js/script1.js", "public, maxAge=4"},
   157  	}
   158  
   159  	for _, test := range cases {
   160  		etag := etagFor(test.path)
   161  		url := mustUrl(test.url)
   162  		header := newHeader("Accept-Encoding", "xxxx, yyy, zzz")
   163  		request := &http.Request{Method: "GET", URL: url, Header: header}
   164  		a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second)
   165  		w := httptest.NewRecorder()
   166  
   167  		a.ServeHTTP(w, request)
   168  
   169  		isEqual(t, w.Code, 200, test.path)
   170  		headers := w.Header()
   171  		//t.Logf("%+v\n", headers)
   172  		isGte(t, len(headers), 6, test.path)
   173  		isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path)
   174  		isEqual(t, headers["Content-Type"], []string{test.mime}, test.path)
   175  		isEqual(t, headers["Content-Encoding"], emptyStrings, test.path)
   176  		isEqual(t, headers["Vary"], emptyStrings, test.path)
   177  		isEqual(t, headers["Etag"], []string{etag}, test.path)
   178  		isEqual(t, len(headers["Expires"]), 1, test.path)
   179  		isGte(t, len(headers["Expires"][0]), 25, test.path)
   180  	}
   181  }
   182  
   183  func TestServeHTTP200WithGzipAcceptHeaderButNoGzippedFile(t *testing.T) {
   184  	cases := []struct {
   185  		n                             int
   186  		maxAge                        time.Duration
   187  		url, mime, path, cacheControl string
   188  	}{
   189  		{0, 1, "http://localhost:8001/css/style2.css", "text/css; charset=utf-8", "assets/css/style2.css", "public, maxAge=1"},
   190  		{2, 2, "http://localhost:8001/a/b/css/style2.css", "text/css; charset=utf-8", "assets/css/style2.css", "public, maxAge=2"},
   191  		{0, 3, "http://localhost:8001/js/script2.js", "application/javascript", "assets/js/script2.js", "public, maxAge=3"},
   192  		{2, 4, "http://localhost:8001/a/b/js/script2.js", "application/javascript", "assets/js/script2.js", "public, maxAge=4"},
   193  		{0, 5, "http://localhost:8001/img/sort_asc.png", "image/png", "assets/img/sort_asc.png", "public, maxAge=5"},
   194  		{2, 6, "http://localhost:8001/a/b/img/sort_asc.png", "image/png", "assets/img/sort_asc.png", "public, maxAge=6"},
   195  	}
   196  
   197  	for _, test := range cases {
   198  		etag := etagFor(test.path)
   199  		url := mustUrl(test.url)
   200  		header := newHeader("Accept-Encoding", "xxxx, gzip, zzz")
   201  		request := &http.Request{Method: "GET", URL: url, Header: header}
   202  		a := NewAssetHandler("./assets/").StripOff(test.n).WithMaxAge(test.maxAge * time.Second)
   203  		w := httptest.NewRecorder()
   204  
   205  		a.ServeHTTP(w, request)
   206  
   207  		isEqual(t, w.Code, 200, test.path)
   208  		headers := w.Header()
   209  		//t.Logf("%+v\n", headers)
   210  		isGte(t, len(headers), 6, test.path)
   211  		isEqual(t, headers["Cache-Control"], []string{test.cacheControl}, test.path)
   212  		isEqual(t, headers["Content-Type"], []string{test.mime}, test.path)
   213  		isEqual(t, headers["Content-Encoding"], emptyStrings, test.path)
   214  		isEqual(t, headers["Vary"], emptyStrings, test.path)
   215  		isEqual(t, headers["Etag"], []string{etag}, test.path)
   216  		isEqual(t, len(headers["Expires"]), 1, test.path)
   217  		isGte(t, len(headers["Expires"][0]), 25, test.path)
   218  	}
   219  }
   220  
   221  //-------------------------------------------------------------------------------------------------
   222  
   223  type h404 struct{}
   224  
   225  func (h *h404) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   226  	w.Header().Set("Content-Type", "text/html")
   227  	w.WriteHeader(404)
   228  	w.Write([]byte("<html>foo</html>"))
   229  }
   230  
   231  func Test404Handler(t *testing.T) {
   232  	cases := []struct {
   233  		path, conType, response string
   234  		notFound                http.Handler
   235  	}{
   236  		{"/img/nonexisting.png", "text/plain; charset=utf-8", "404 Not found\n", nil},
   237  		{"/img/nonexisting.png", "text/html", "<html>foo</html>", &h404{}},
   238  	}
   239  
   240  	for i, test := range cases {
   241  		url := mustUrl("http://localhost:8001" + test.path)
   242  		request := &http.Request{Method: "GET", URL: url}
   243  		a := NewAssetHandler("./assets/").WithNotFound(test.notFound)
   244  		isEqual(t, a.NotFound, test.notFound, i)
   245  		w := httptest.NewRecorder()
   246  
   247  		a.ServeHTTP(w, request)
   248  
   249  		isEqual(t, w.Code, 404, i)
   250  		isEqual(t, w.Header().Get("Content-Type"), test.conType, i)
   251  		isEqual(t, w.Body.String(), test.response, i)
   252  	}
   253  }
   254  
   255  func Test403Handling(t *testing.T) {
   256  	cases := []struct {
   257  		path   string
   258  		header http.Header
   259  	}{
   260  		{"http://localhost:8001/css/style1.css", newHeader()},
   261  		{"http://localhost:8001/css/style1.css", newHeader("Accept-Encoding", "gzip")},
   262  	}
   263  
   264  	for i, test := range cases {
   265  		url := mustUrl("http://localhost:8001" + test.path)
   266  		request := &http.Request{Method: "GET", URL: url, Header: test.header}
   267  		a := NewAssetHandlerFS(&fs403{os.ErrPermission})
   268  		w := httptest.NewRecorder()
   269  
   270  		a.ServeHTTP(w, request)
   271  
   272  		isEqual(t, w.Code, 403, i)
   273  		isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i)
   274  		isEqual(t, w.Body.String(), "403 Forbidden\n", i)
   275  	}
   276  }
   277  
   278  func Test503Handling(t *testing.T) {
   279  	cases := []struct {
   280  		path   string
   281  		header http.Header
   282  	}{
   283  		{"http://localhost:8001/css/style1.css", newHeader()},
   284  		{"http://localhost:8001/css/style1.css", newHeader("Accept-Encoding", "gzip")},
   285  	}
   286  
   287  	for i, test := range cases {
   288  		url := mustUrl("http://localhost:8001" + test.path)
   289  		request := &http.Request{Method: "GET", URL: url, Header: test.header}
   290  		a := NewAssetHandlerFS(&fs403{os.ErrInvalid})
   291  		w := httptest.NewRecorder()
   292  
   293  		a.ServeHTTP(w, request)
   294  
   295  		isEqual(t, w.Code, 503, i)
   296  		isEqual(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8", i)
   297  		isNotEqual(t, w.Header().Get("Retry-After"), "", i)
   298  		isEqual(t, w.Body.String(), "503 Service unavailable\n", i)
   299  	}
   300  }
   301  
   302  //-------------------------------------------------------------------------------------------------
   303  
   304  func TestServeHTTP304(t *testing.T) {
   305  	cases := []struct {
   306  		url, path string
   307  		notFound  http.Handler
   308  	}{
   309  		{"http://localhost:8001/css/style1.css", "assets/css/style1.css.gz", nil},
   310  		{"http://localhost:8001/css/style2.css", "assets/css/style2.css", nil},
   311  		{"http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", nil},
   312  		{"http://localhost:8001/js/script1.js", "assets/js/script1.js.gz", nil},
   313  		{"http://localhost:8001/js/script2.js", "assets/js/script2.js", nil},
   314  
   315  		{"http://localhost:8001/css/style1.css", "assets/css/style1.css.gz", &h404{}},
   316  		{"http://localhost:8001/css/style2.css", "assets/css/style2.css", &h404{}},
   317  		{"http://localhost:8001/img/sort_asc.png", "assets/img/sort_asc.png", &h404{}},
   318  		{"http://localhost:8001/js/script1.js", "assets/js/script1.js.gz", &h404{}},
   319  		{"http://localhost:8001/js/script2.js", "assets/js/script2.js", &h404{}},
   320  	}
   321  
   322  	// net/http serveFiles handles conditional requests according to RFC723x specs.
   323  	// So we only need to check that a conditional request is correctly wired in.
   324  
   325  	for i, test := range cases {
   326  		etag := etagFor(test.path)
   327  		url := mustUrl(test.url)
   328  		header := newHeader("Accept-Encoding", "gzip", "If-None-Match", etag)
   329  		request := &http.Request{Method: "GET", URL: url, Header: header}
   330  		a := NewAssetHandler("./assets/").WithNotFound(test.notFound)
   331  		w := httptest.NewRecorder()
   332  
   333  		a.ServeHTTP(w, request)
   334  
   335  		isEqual(t, w.Code, 304, i)
   336  		headers := w.Header()
   337  		//t.Logf("%+v\n", headers)
   338  		isGte(t, len(headers), 1, i)
   339  		isEqual(t, headers["Cache-Control"], emptyStrings, i)
   340  		isEqual(t, headers["Content-Type"], emptyStrings, i)
   341  		isEqual(t, headers["Content-Length"], emptyStrings, i)
   342  		if strings.HasSuffix(test.path, ".gz") {
   343  			isEqual(t, headers["Content-Encoding"], []string{"gzip"}, i)
   344  			isEqual(t, headers["Vary"], []string{"Accept-Encoding"}, i)
   345  			isEqual(t, headers["Etag"], []string{etag}, i)
   346  		} else {
   347  			isEqual(t, headers["Content-Encoding"], emptyStrings, i)
   348  			isEqual(t, headers["Vary"], emptyStrings, i)
   349  			isEqual(t, headers["Etag"], []string{etag}, i)
   350  		}
   351  	}
   352  }
   353  
   354  //-------------------------------------------------------------------------------------------------
   355  
   356  func Benchmark(t *testing.B) {
   357  	t.StopTimer()
   358  
   359  	cases := []struct {
   360  		strip       int
   361  		url, enc    string
   362  		sendEtagFor string
   363  		code        int
   364  	}{
   365  		{0, "css/style1.css", "gzip", "", 200},                             // has Gzip
   366  		{1, "a/css/style1.css", "gzip", "", 200},                           // has Gzip
   367  		{2, "a/b/css/style1.css", "gzip", "", 200},                         // has Gzip
   368  		{2, "a/b/css/style1.css", "xxxx", "", 200},                         // has Gzip
   369  		{2, "a/b/css/style1.css", "gzip", "assets/css/style1.css.gz", 304}, // has Gzip
   370  		{2, "a/b/css/style1.css", "xxxx", "assets/css/style1.css", 304},    // has Gzip
   371  
   372  		{2, "a/b/css/style2.css", "gzip", "", 200},
   373  		{2, "a/b/css/style2.css", "xxxx", "", 200},
   374  		{2, "a/b/css/style2.css", "gzip", "assets/css/style2.css", 304},
   375  		{2, "a/a/css/style2.css", "xxxx", "assets/css/style2.css", 304},
   376  
   377  		{2, "a/b/js/script1.js", "gzip", "", 200},                        // has gzip
   378  		{2, "a/b/js/script1.js", "xxxx", "", 200},                        // has gzip
   379  		{2, "a/b/js/script1.js", "gzip", "assets/js/script1.js.gz", 304}, // has gzip
   380  		{2, "a/a/js/script1.js", "xxxx", "assets/js/script1.js", 304},    // has gzip
   381  
   382  		{2, "a/b/js/script2.js", "gzip", "", 200},
   383  		{2, "a/b/js/script2.js", "xxxx", "", 200},
   384  		{2, "a/b/js/script2.js", "gzip", "assets/js/script2.js", 304},
   385  		{2, "a/a/js/script2.js", "xxxx", "assets/js/script2.js", 304},
   386  
   387  		{2, "a/b/img/sort_asc.png", "gzip", "", 200},
   388  		{2, "a/b/img/sort_asc.png", "xxxx", "", 200},
   389  		{2, "a/b/img/sort_asc.png", "gzip", "assets/img/sort_asc.png", 304},
   390  		{2, "a/a/img/sort_asc.png", "xxxx", "assets/img/sort_asc.png", 304},
   391  
   392  		{2, "a/b/img/nonexisting.png", "gzip", "", 404},
   393  		{2, "a/b/img/nonexisting.png", "xxxx", "", 404},
   394  	}
   395  
   396  	ages := []time.Duration{0, time.Hour}
   397  
   398  	for _, test := range cases {
   399  		header := newHeader("Accept-Encoding", test.enc)
   400  		etagOn := "no-etag"
   401  		if test.sendEtagFor != "" {
   402  			header = newHeader("Accept-Encoding", test.enc, "If-None-Match", etagFor(test.sendEtagFor))
   403  			etagOn = "etag"
   404  		}
   405  
   406  		for _, age := range ages {
   407  			a := NewAssetHandler("./assets/").StripOff(test.strip).WithMaxAge(age)
   408  
   409  			t.Run(fmt.Sprintf("%s~%s~%v~%d~%v", test.url, test.enc, etagOn, test.code, age), func(b *testing.B) {
   410  				b.StopTimer()
   411  
   412  				for i := 0; i < b.N; i++ {
   413  					url := mustUrl("http://localhost:8001/" + test.url)
   414  					request := &http.Request{Method: "GET", URL: url, Header: header}
   415  					w := httptest.NewRecorder()
   416  
   417  					b.StartTimer()
   418  					a.ServeHTTP(w, request)
   419  					b.StopTimer()
   420  
   421  					if w.Code != test.code {
   422  						b.Fatalf("Expected %d but got %d", test.code, w.Code)
   423  					}
   424  				}
   425  			})
   426  		}
   427  	}
   428  }
   429  
   430  //-------------------------------------------------------------------------------------------------
   431  
   432  func isEqual(t *testing.T, a, b, hint interface{}) {
   433  	t.Helper()
   434  	if !reflect.DeepEqual(a, b) {
   435  		t.Errorf("Got %#v; expected %#v - for %v\n", a, b, hint)
   436  	}
   437  }
   438  
   439  func isNotEqual(t *testing.T, a, b, hint interface{}) {
   440  	t.Helper()
   441  	if reflect.DeepEqual(a, b) {
   442  		t.Errorf("Got %#v; expected something else - for %v\n", a, hint)
   443  	}
   444  }
   445  
   446  func isGte(t *testing.T, a, b int, hint interface{}) {
   447  	t.Helper()
   448  	if a < b {
   449  		t.Errorf("Got %d; expected at least %d - for %v\n", a, b, hint)
   450  	}
   451  }
   452  
   453  func mustUrl(s string) *URL {
   454  	parsed, err := Parse(s)
   455  	must(err)
   456  	return parsed
   457  }
   458  
   459  func newHeader(kv ...string) http.Header {
   460  	header := make(http.Header)
   461  	for i, x := range kv {
   462  		if i%2 == 0 {
   463  			header[x] = []string{kv[i+1]}
   464  		}
   465  	}
   466  	return header
   467  }
   468  
   469  // must abort the program on error, printing a stack trace.
   470  func must(err error) {
   471  	if err != nil {
   472  		panic(err)
   473  	}
   474  }
   475  
   476  func mustStat(name string) os.FileInfo {
   477  	d, err := os.Stat(name)
   478  	if err != nil {
   479  		panic(err)
   480  	}
   481  	return d
   482  }
   483  
   484  func etagFor(name string) string {
   485  	d := mustStat(name)
   486  	t := ""
   487  	if strings.HasSuffix(name, ".gz") {
   488  		t = "W/" // weak etag
   489  	}
   490  	return fmt.Sprintf(`%s"%x-%x"`, t, d.ModTime().Unix(), d.Size())
   491  }
   492  
   493  //-------------------------------------------------------------------------------------------------
   494  
   495  type fs403 struct {
   496  	err error
   497  }
   498  
   499  func (fs fs403) Create(name string) (afero.File, error) {
   500  	return nil, fs.err
   501  }
   502  
   503  func (fs fs403) Mkdir(name string, perm os.FileMode) error {
   504  	return fs.err
   505  }
   506  
   507  func (fs fs403) MkdirAll(path string, perm os.FileMode) error {
   508  	return fs.err
   509  }
   510  
   511  func (fs fs403) Open(name string) (afero.File, error) {
   512  	return nil, fs.err
   513  }
   514  
   515  func (fs fs403) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
   516  	return nil, fs.err
   517  }
   518  
   519  func (fs fs403) Remove(name string) error {
   520  	return fs.err
   521  }
   522  
   523  func (fs fs403) RemoveAll(path string) error {
   524  	return fs.err
   525  }
   526  
   527  func (fs fs403) Rename(oldname, newname string) error {
   528  	return fs.err
   529  }
   530  
   531  func (fs fs403) Stat(name string) (os.FileInfo, error) {
   532  	return nil, fs.err
   533  }
   534  
   535  func (fs403) Name() string {
   536  	return "dumb"
   537  }
   538  
   539  func (fs fs403) Chmod(name string, mode os.FileMode) error {
   540  	return fs.err
   541  }
   542  
   543  func (fs fs403) Chtimes(name string, atime time.Time, mtime time.Time) error {
   544  	return fs.err
   545  }