gitee.com/ks-custle/core-gm@v0.0.0-20230922171213-b83bdd97b62c/net/webdav/prop_test.go (about)

     1  // Copyright 2015 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 webdav
     6  
     7  import (
     8  	"context"
     9  	"encoding/xml"
    10  	"fmt"
    11  	"os"
    12  	"reflect"
    13  	"regexp"
    14  	"sort"
    15  	"testing"
    16  
    17  	http "gitee.com/ks-custle/core-gm/gmhttp"
    18  )
    19  
    20  func TestMemPS(t *testing.T) {
    21  	ctx := context.Background()
    22  	// calcProps calculates the getlastmodified and getetag DAV: property
    23  	// values in pstats for resource name in file-system fs.
    24  	calcProps := func(name string, fs FileSystem, ls LockSystem, pstats []Propstat) error {
    25  		fi, err := fs.Stat(ctx, name)
    26  		if err != nil {
    27  			return err
    28  		}
    29  		for _, pst := range pstats {
    30  			for i, p := range pst.Props {
    31  				switch p.XMLName {
    32  				case xml.Name{Space: "DAV:", Local: "getlastmodified"}:
    33  					p.InnerXML = []byte(fi.ModTime().UTC().Format(http.TimeFormat))
    34  					pst.Props[i] = p
    35  				case xml.Name{Space: "DAV:", Local: "getetag"}:
    36  					if fi.IsDir() {
    37  						continue
    38  					}
    39  					etag, err := findETag(ctx, fs, ls, name, fi)
    40  					if err != nil {
    41  						return err
    42  					}
    43  					p.InnerXML = []byte(etag)
    44  					pst.Props[i] = p
    45  				}
    46  			}
    47  		}
    48  		return nil
    49  	}
    50  
    51  	const (
    52  		lockEntry = `` +
    53  			`<D:lockentry xmlns:D="DAV:">` +
    54  			`<D:lockscope><D:exclusive/></D:lockscope>` +
    55  			`<D:locktype><D:write/></D:locktype>` +
    56  			`</D:lockentry>`
    57  		statForbiddenError = `<D:cannot-modify-protected-property xmlns:D="DAV:"/>`
    58  	)
    59  
    60  	type propOp struct {
    61  		op            string
    62  		name          string
    63  		pnames        []xml.Name
    64  		patches       []Proppatch
    65  		wantPnames    []xml.Name
    66  		wantPropstats []Propstat
    67  	}
    68  
    69  	testCases := []struct {
    70  		desc        string
    71  		noDeadProps bool
    72  		buildfs     []string
    73  		propOp      []propOp
    74  	}{{
    75  		desc:    "propname",
    76  		buildfs: []string{"mkdir /dir", "touch /file"},
    77  		propOp: []propOp{{
    78  			op:   "propname",
    79  			name: "/dir",
    80  			wantPnames: []xml.Name{
    81  				{Space: "DAV:", Local: "resourcetype"},
    82  				{Space: "DAV:", Local: "displayname"},
    83  				{Space: "DAV:", Local: "supportedlock"},
    84  				{Space: "DAV:", Local: "getlastmodified"},
    85  			},
    86  		}, {
    87  			op:   "propname",
    88  			name: "/file",
    89  			wantPnames: []xml.Name{
    90  				{Space: "DAV:", Local: "resourcetype"},
    91  				{Space: "DAV:", Local: "displayname"},
    92  				{Space: "DAV:", Local: "getcontentlength"},
    93  				{Space: "DAV:", Local: "getlastmodified"},
    94  				{Space: "DAV:", Local: "getcontenttype"},
    95  				{Space: "DAV:", Local: "getetag"},
    96  				{Space: "DAV:", Local: "supportedlock"},
    97  			},
    98  		}},
    99  	}, {
   100  		desc:    "allprop dir and file",
   101  		buildfs: []string{"mkdir /dir", "write /file foobarbaz"},
   102  		propOp: []propOp{{
   103  			op:   "allprop",
   104  			name: "/dir",
   105  			wantPropstats: []Propstat{{
   106  				Status: http.StatusOK,
   107  				Props: []Property{{
   108  					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},
   109  					InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),
   110  				}, {
   111  					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},
   112  					InnerXML: []byte("dir"),
   113  				}, {
   114  					XMLName:  xml.Name{Space: "DAV:", Local: "getlastmodified"},
   115  					InnerXML: nil, // Calculated during test.
   116  				}, {
   117  					XMLName:  xml.Name{Space: "DAV:", Local: "supportedlock"},
   118  					InnerXML: []byte(lockEntry),
   119  				}},
   120  			}},
   121  		}, {
   122  			op:   "allprop",
   123  			name: "/file",
   124  			wantPropstats: []Propstat{{
   125  				Status: http.StatusOK,
   126  				Props: []Property{{
   127  					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},
   128  					InnerXML: []byte(""),
   129  				}, {
   130  					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},
   131  					InnerXML: []byte("file"),
   132  				}, {
   133  					XMLName:  xml.Name{Space: "DAV:", Local: "getcontentlength"},
   134  					InnerXML: []byte("9"),
   135  				}, {
   136  					XMLName:  xml.Name{Space: "DAV:", Local: "getlastmodified"},
   137  					InnerXML: nil, // Calculated during test.
   138  				}, {
   139  					XMLName:  xml.Name{Space: "DAV:", Local: "getcontenttype"},
   140  					InnerXML: []byte("text/plain; charset=utf-8"),
   141  				}, {
   142  					XMLName:  xml.Name{Space: "DAV:", Local: "getetag"},
   143  					InnerXML: nil, // Calculated during test.
   144  				}, {
   145  					XMLName:  xml.Name{Space: "DAV:", Local: "supportedlock"},
   146  					InnerXML: []byte(lockEntry),
   147  				}},
   148  			}},
   149  		}, {
   150  			op:   "allprop",
   151  			name: "/file",
   152  			pnames: []xml.Name{
   153  				{"DAV:", "resourcetype"},
   154  				{"foo", "bar"},
   155  			},
   156  			wantPropstats: []Propstat{{
   157  				Status: http.StatusOK,
   158  				Props: []Property{{
   159  					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},
   160  					InnerXML: []byte(""),
   161  				}, {
   162  					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},
   163  					InnerXML: []byte("file"),
   164  				}, {
   165  					XMLName:  xml.Name{Space: "DAV:", Local: "getcontentlength"},
   166  					InnerXML: []byte("9"),
   167  				}, {
   168  					XMLName:  xml.Name{Space: "DAV:", Local: "getlastmodified"},
   169  					InnerXML: nil, // Calculated during test.
   170  				}, {
   171  					XMLName:  xml.Name{Space: "DAV:", Local: "getcontenttype"},
   172  					InnerXML: []byte("text/plain; charset=utf-8"),
   173  				}, {
   174  					XMLName:  xml.Name{Space: "DAV:", Local: "getetag"},
   175  					InnerXML: nil, // Calculated during test.
   176  				}, {
   177  					XMLName:  xml.Name{Space: "DAV:", Local: "supportedlock"},
   178  					InnerXML: []byte(lockEntry),
   179  				}}}, {
   180  				Status: http.StatusNotFound,
   181  				Props: []Property{{
   182  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   183  				}}},
   184  			},
   185  		}},
   186  	}, {
   187  		desc:    "propfind DAV:resourcetype",
   188  		buildfs: []string{"mkdir /dir", "touch /file"},
   189  		propOp: []propOp{{
   190  			op:     "propfind",
   191  			name:   "/dir",
   192  			pnames: []xml.Name{{"DAV:", "resourcetype"}},
   193  			wantPropstats: []Propstat{{
   194  				Status: http.StatusOK,
   195  				Props: []Property{{
   196  					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},
   197  					InnerXML: []byte(`<D:collection xmlns:D="DAV:"/>`),
   198  				}},
   199  			}},
   200  		}, {
   201  			op:     "propfind",
   202  			name:   "/file",
   203  			pnames: []xml.Name{{"DAV:", "resourcetype"}},
   204  			wantPropstats: []Propstat{{
   205  				Status: http.StatusOK,
   206  				Props: []Property{{
   207  					XMLName:  xml.Name{Space: "DAV:", Local: "resourcetype"},
   208  					InnerXML: []byte(""),
   209  				}},
   210  			}},
   211  		}},
   212  	}, {
   213  		desc:    "propfind unsupported DAV properties",
   214  		buildfs: []string{"mkdir /dir"},
   215  		propOp: []propOp{{
   216  			op:     "propfind",
   217  			name:   "/dir",
   218  			pnames: []xml.Name{{"DAV:", "getcontentlanguage"}},
   219  			wantPropstats: []Propstat{{
   220  				Status: http.StatusNotFound,
   221  				Props: []Property{{
   222  					XMLName: xml.Name{Space: "DAV:", Local: "getcontentlanguage"},
   223  				}},
   224  			}},
   225  		}, {
   226  			op:     "propfind",
   227  			name:   "/dir",
   228  			pnames: []xml.Name{{"DAV:", "creationdate"}},
   229  			wantPropstats: []Propstat{{
   230  				Status: http.StatusNotFound,
   231  				Props: []Property{{
   232  					XMLName: xml.Name{Space: "DAV:", Local: "creationdate"},
   233  				}},
   234  			}},
   235  		}},
   236  	}, {
   237  		desc:    "propfind getetag for files but not for directories",
   238  		buildfs: []string{"mkdir /dir", "touch /file"},
   239  		propOp: []propOp{{
   240  			op:     "propfind",
   241  			name:   "/dir",
   242  			pnames: []xml.Name{{"DAV:", "getetag"}},
   243  			wantPropstats: []Propstat{{
   244  				Status: http.StatusNotFound,
   245  				Props: []Property{{
   246  					XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
   247  				}},
   248  			}},
   249  		}, {
   250  			op:     "propfind",
   251  			name:   "/file",
   252  			pnames: []xml.Name{{"DAV:", "getetag"}},
   253  			wantPropstats: []Propstat{{
   254  				Status: http.StatusOK,
   255  				Props: []Property{{
   256  					XMLName:  xml.Name{Space: "DAV:", Local: "getetag"},
   257  					InnerXML: nil, // Calculated during test.
   258  				}},
   259  			}},
   260  		}},
   261  	}, {
   262  		desc:        "proppatch property on no-dead-properties file system",
   263  		buildfs:     []string{"mkdir /dir"},
   264  		noDeadProps: true,
   265  		propOp: []propOp{{
   266  			op:   "proppatch",
   267  			name: "/dir",
   268  			patches: []Proppatch{{
   269  				Props: []Property{{
   270  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   271  				}},
   272  			}},
   273  			wantPropstats: []Propstat{{
   274  				Status: http.StatusForbidden,
   275  				Props: []Property{{
   276  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   277  				}},
   278  			}},
   279  		}, {
   280  			op:   "proppatch",
   281  			name: "/dir",
   282  			patches: []Proppatch{{
   283  				Props: []Property{{
   284  					XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
   285  				}},
   286  			}},
   287  			wantPropstats: []Propstat{{
   288  				Status:   http.StatusForbidden,
   289  				XMLError: statForbiddenError,
   290  				Props: []Property{{
   291  					XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
   292  				}},
   293  			}},
   294  		}},
   295  	}, {
   296  		desc:    "proppatch dead property",
   297  		buildfs: []string{"mkdir /dir"},
   298  		propOp: []propOp{{
   299  			op:   "proppatch",
   300  			name: "/dir",
   301  			patches: []Proppatch{{
   302  				Props: []Property{{
   303  					XMLName:  xml.Name{Space: "foo", Local: "bar"},
   304  					InnerXML: []byte("baz"),
   305  				}},
   306  			}},
   307  			wantPropstats: []Propstat{{
   308  				Status: http.StatusOK,
   309  				Props: []Property{{
   310  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   311  				}},
   312  			}},
   313  		}, {
   314  			op:     "propfind",
   315  			name:   "/dir",
   316  			pnames: []xml.Name{{Space: "foo", Local: "bar"}},
   317  			wantPropstats: []Propstat{{
   318  				Status: http.StatusOK,
   319  				Props: []Property{{
   320  					XMLName:  xml.Name{Space: "foo", Local: "bar"},
   321  					InnerXML: []byte("baz"),
   322  				}},
   323  			}},
   324  		}},
   325  	}, {
   326  		desc:    "proppatch dead property with failed dependency",
   327  		buildfs: []string{"mkdir /dir"},
   328  		propOp: []propOp{{
   329  			op:   "proppatch",
   330  			name: "/dir",
   331  			patches: []Proppatch{{
   332  				Props: []Property{{
   333  					XMLName:  xml.Name{Space: "foo", Local: "bar"},
   334  					InnerXML: []byte("baz"),
   335  				}},
   336  			}, {
   337  				Props: []Property{{
   338  					XMLName:  xml.Name{Space: "DAV:", Local: "displayname"},
   339  					InnerXML: []byte("xxx"),
   340  				}},
   341  			}},
   342  			wantPropstats: []Propstat{{
   343  				Status:   http.StatusForbidden,
   344  				XMLError: statForbiddenError,
   345  				Props: []Property{{
   346  					XMLName: xml.Name{Space: "DAV:", Local: "displayname"},
   347  				}},
   348  			}, {
   349  				Status: StatusFailedDependency,
   350  				Props: []Property{{
   351  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   352  				}},
   353  			}},
   354  		}, {
   355  			op:     "propfind",
   356  			name:   "/dir",
   357  			pnames: []xml.Name{{Space: "foo", Local: "bar"}},
   358  			wantPropstats: []Propstat{{
   359  				Status: http.StatusNotFound,
   360  				Props: []Property{{
   361  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   362  				}},
   363  			}},
   364  		}},
   365  	}, {
   366  		desc:    "proppatch remove dead property",
   367  		buildfs: []string{"mkdir /dir"},
   368  		propOp: []propOp{{
   369  			op:   "proppatch",
   370  			name: "/dir",
   371  			patches: []Proppatch{{
   372  				Props: []Property{{
   373  					XMLName:  xml.Name{Space: "foo", Local: "bar"},
   374  					InnerXML: []byte("baz"),
   375  				}, {
   376  					XMLName:  xml.Name{Space: "spam", Local: "ham"},
   377  					InnerXML: []byte("eggs"),
   378  				}},
   379  			}},
   380  			wantPropstats: []Propstat{{
   381  				Status: http.StatusOK,
   382  				Props: []Property{{
   383  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   384  				}, {
   385  					XMLName: xml.Name{Space: "spam", Local: "ham"},
   386  				}},
   387  			}},
   388  		}, {
   389  			op:   "propfind",
   390  			name: "/dir",
   391  			pnames: []xml.Name{
   392  				{Space: "foo", Local: "bar"},
   393  				{Space: "spam", Local: "ham"},
   394  			},
   395  			wantPropstats: []Propstat{{
   396  				Status: http.StatusOK,
   397  				Props: []Property{{
   398  					XMLName:  xml.Name{Space: "foo", Local: "bar"},
   399  					InnerXML: []byte("baz"),
   400  				}, {
   401  					XMLName:  xml.Name{Space: "spam", Local: "ham"},
   402  					InnerXML: []byte("eggs"),
   403  				}},
   404  			}},
   405  		}, {
   406  			op:   "proppatch",
   407  			name: "/dir",
   408  			patches: []Proppatch{{
   409  				Remove: true,
   410  				Props: []Property{{
   411  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   412  				}},
   413  			}},
   414  			wantPropstats: []Propstat{{
   415  				Status: http.StatusOK,
   416  				Props: []Property{{
   417  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   418  				}},
   419  			}},
   420  		}, {
   421  			op:   "propfind",
   422  			name: "/dir",
   423  			pnames: []xml.Name{
   424  				{Space: "foo", Local: "bar"},
   425  				{Space: "spam", Local: "ham"},
   426  			},
   427  			wantPropstats: []Propstat{{
   428  				Status: http.StatusNotFound,
   429  				Props: []Property{{
   430  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   431  				}},
   432  			}, {
   433  				Status: http.StatusOK,
   434  				Props: []Property{{
   435  					XMLName:  xml.Name{Space: "spam", Local: "ham"},
   436  					InnerXML: []byte("eggs"),
   437  				}},
   438  			}},
   439  		}},
   440  	}, {
   441  		desc:    "propname with dead property",
   442  		buildfs: []string{"touch /file"},
   443  		propOp: []propOp{{
   444  			op:   "proppatch",
   445  			name: "/file",
   446  			patches: []Proppatch{{
   447  				Props: []Property{{
   448  					XMLName:  xml.Name{Space: "foo", Local: "bar"},
   449  					InnerXML: []byte("baz"),
   450  				}},
   451  			}},
   452  			wantPropstats: []Propstat{{
   453  				Status: http.StatusOK,
   454  				Props: []Property{{
   455  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   456  				}},
   457  			}},
   458  		}, {
   459  			op:   "propname",
   460  			name: "/file",
   461  			wantPnames: []xml.Name{
   462  				{Space: "DAV:", Local: "resourcetype"},
   463  				{Space: "DAV:", Local: "displayname"},
   464  				{Space: "DAV:", Local: "getcontentlength"},
   465  				{Space: "DAV:", Local: "getlastmodified"},
   466  				{Space: "DAV:", Local: "getcontenttype"},
   467  				{Space: "DAV:", Local: "getetag"},
   468  				{Space: "DAV:", Local: "supportedlock"},
   469  				{Space: "foo", Local: "bar"},
   470  			},
   471  		}},
   472  	}, {
   473  		desc:    "proppatch remove unknown dead property",
   474  		buildfs: []string{"mkdir /dir"},
   475  		propOp: []propOp{{
   476  			op:   "proppatch",
   477  			name: "/dir",
   478  			patches: []Proppatch{{
   479  				Remove: true,
   480  				Props: []Property{{
   481  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   482  				}},
   483  			}},
   484  			wantPropstats: []Propstat{{
   485  				Status: http.StatusOK,
   486  				Props: []Property{{
   487  					XMLName: xml.Name{Space: "foo", Local: "bar"},
   488  				}},
   489  			}},
   490  		}},
   491  	}, {
   492  		desc:    "bad: propfind unknown property",
   493  		buildfs: []string{"mkdir /dir"},
   494  		propOp: []propOp{{
   495  			op:     "propfind",
   496  			name:   "/dir",
   497  			pnames: []xml.Name{{"foo:", "bar"}},
   498  			wantPropstats: []Propstat{{
   499  				Status: http.StatusNotFound,
   500  				Props: []Property{{
   501  					XMLName: xml.Name{Space: "foo:", Local: "bar"},
   502  				}},
   503  			}},
   504  		}},
   505  	}}
   506  
   507  	for _, tc := range testCases {
   508  		fs, err := buildTestFS(tc.buildfs)
   509  		if err != nil {
   510  			t.Fatalf("%s: cannot create test filesystem: %v", tc.desc, err)
   511  		}
   512  		if tc.noDeadProps {
   513  			fs = noDeadPropsFS{fs}
   514  		}
   515  		ls := NewMemLS()
   516  		for _, op := range tc.propOp {
   517  			desc := fmt.Sprintf("%s: %s %s", tc.desc, op.op, op.name)
   518  			if err = calcProps(op.name, fs, ls, op.wantPropstats); err != nil {
   519  				t.Fatalf("%s: calcProps: %v", desc, err)
   520  			}
   521  
   522  			// Call property system.
   523  			var propstats []Propstat
   524  			switch op.op {
   525  			case "propname":
   526  				pnames, err := propnames(ctx, fs, ls, op.name)
   527  				if err != nil {
   528  					t.Errorf("%s: got error %v, want nil", desc, err)
   529  					continue
   530  				}
   531  				sort.Sort(byXMLName(pnames))
   532  				sort.Sort(byXMLName(op.wantPnames))
   533  				if !reflect.DeepEqual(pnames, op.wantPnames) {
   534  					t.Errorf("%s: pnames\ngot  %q\nwant %q", desc, pnames, op.wantPnames)
   535  				}
   536  				continue
   537  			case "allprop":
   538  				propstats, err = allprop(ctx, fs, ls, op.name, op.pnames)
   539  			case "propfind":
   540  				propstats, err = props(ctx, fs, ls, op.name, op.pnames)
   541  			case "proppatch":
   542  				propstats, err = patch(ctx, fs, ls, op.name, op.patches)
   543  			default:
   544  				t.Fatalf("%s: %s not implemented", desc, op.op)
   545  			}
   546  			if err != nil {
   547  				t.Errorf("%s: got error %v, want nil", desc, err)
   548  				continue
   549  			}
   550  			// Compare return values from allprop, propfind or proppatch.
   551  			for _, pst := range propstats {
   552  				sort.Sort(byPropname(pst.Props))
   553  			}
   554  			for _, pst := range op.wantPropstats {
   555  				sort.Sort(byPropname(pst.Props))
   556  			}
   557  			sort.Sort(byStatus(propstats))
   558  			sort.Sort(byStatus(op.wantPropstats))
   559  			if !reflect.DeepEqual(propstats, op.wantPropstats) {
   560  				t.Errorf("%s: propstat\ngot  %q\nwant %q", desc, propstats, op.wantPropstats)
   561  			}
   562  		}
   563  	}
   564  }
   565  
   566  func cmpXMLName(a, b xml.Name) bool {
   567  	if a.Space != b.Space {
   568  		return a.Space < b.Space
   569  	}
   570  	return a.Local < b.Local
   571  }
   572  
   573  type byXMLName []xml.Name
   574  
   575  func (b byXMLName) Len() int           { return len(b) }
   576  func (b byXMLName) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   577  func (b byXMLName) Less(i, j int) bool { return cmpXMLName(b[i], b[j]) }
   578  
   579  type byPropname []Property
   580  
   581  func (b byPropname) Len() int           { return len(b) }
   582  func (b byPropname) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   583  func (b byPropname) Less(i, j int) bool { return cmpXMLName(b[i].XMLName, b[j].XMLName) }
   584  
   585  type byStatus []Propstat
   586  
   587  func (b byStatus) Len() int           { return len(b) }
   588  func (b byStatus) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   589  func (b byStatus) Less(i, j int) bool { return b[i].Status < b[j].Status }
   590  
   591  type noDeadPropsFS struct {
   592  	FileSystem
   593  }
   594  
   595  func (fs noDeadPropsFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (File, error) {
   596  	f, err := fs.FileSystem.OpenFile(ctx, name, flag, perm)
   597  	if err != nil {
   598  		return nil, err
   599  	}
   600  	return noDeadPropsFile{f}, nil
   601  }
   602  
   603  // noDeadPropsFile wraps a File but strips any optional DeadPropsHolder methods
   604  // provided by the underlying File implementation.
   605  type noDeadPropsFile struct {
   606  	f File
   607  }
   608  
   609  func (f noDeadPropsFile) Close() error                              { return f.f.Close() }
   610  func (f noDeadPropsFile) Read(p []byte) (int, error)                { return f.f.Read(p) }
   611  func (f noDeadPropsFile) Readdir(count int) ([]os.FileInfo, error)  { return f.f.Readdir(count) }
   612  func (f noDeadPropsFile) Seek(off int64, whence int) (int64, error) { return f.f.Seek(off, whence) }
   613  func (f noDeadPropsFile) Stat() (os.FileInfo, error)                { return f.f.Stat() }
   614  func (f noDeadPropsFile) Write(p []byte) (int, error)               { return f.f.Write(p) }
   615  
   616  type overrideContentType struct {
   617  	os.FileInfo
   618  	contentType string
   619  	err         error
   620  }
   621  
   622  func (o *overrideContentType) ContentType(ctx context.Context) (string, error) {
   623  	return o.contentType, o.err
   624  }
   625  
   626  func TestFindContentTypeOverride(t *testing.T) {
   627  	fs, err := buildTestFS([]string{"touch /file"})
   628  	if err != nil {
   629  		t.Fatalf("cannot create test filesystem: %v", err)
   630  	}
   631  	ctx := context.Background()
   632  	fi, err := fs.Stat(ctx, "/file")
   633  	if err != nil {
   634  		t.Fatalf("cannot Stat /file: %v", err)
   635  	}
   636  
   637  	// Check non overridden case
   638  	originalContentType, err := findContentType(ctx, fs, nil, "/file", fi)
   639  	if err != nil {
   640  		t.Fatalf("findContentType /file failed: %v", err)
   641  	}
   642  	if originalContentType != "text/plain; charset=utf-8" {
   643  		t.Fatalf("ContentType wrong want %q got %q", "text/plain; charset=utf-8", originalContentType)
   644  	}
   645  
   646  	// Now try overriding the ContentType
   647  	o := &overrideContentType{fi, "OverriddenContentType", nil}
   648  	ContentType, err := findContentType(ctx, fs, nil, "/file", o)
   649  	if err != nil {
   650  		t.Fatalf("findContentType /file failed: %v", err)
   651  	}
   652  	if ContentType != o.contentType {
   653  		t.Fatalf("ContentType wrong want %q got %q", o.contentType, ContentType)
   654  	}
   655  
   656  	// Now return ErrNotImplemented and check we get the original content type
   657  	o = &overrideContentType{fi, "OverriddenContentType", ErrNotImplemented}
   658  	ContentType, err = findContentType(ctx, fs, nil, "/file", o)
   659  	if err != nil {
   660  		t.Fatalf("findContentType /file failed: %v", err)
   661  	}
   662  	if ContentType != originalContentType {
   663  		t.Fatalf("ContentType wrong want %q got %q", originalContentType, ContentType)
   664  	}
   665  }
   666  
   667  type overrideETag struct {
   668  	os.FileInfo
   669  	eTag string
   670  	err  error
   671  }
   672  
   673  func (o *overrideETag) ETag(ctx context.Context) (string, error) {
   674  	return o.eTag, o.err
   675  }
   676  
   677  func TestFindETagOverride(t *testing.T) {
   678  	fs, err := buildTestFS([]string{"touch /file"})
   679  	if err != nil {
   680  		t.Fatalf("cannot create test filesystem: %v", err)
   681  	}
   682  	ctx := context.Background()
   683  	fi, err := fs.Stat(ctx, "/file")
   684  	if err != nil {
   685  		t.Fatalf("cannot Stat /file: %v", err)
   686  	}
   687  
   688  	// Check non overridden case
   689  	originalETag, err := findETag(ctx, fs, nil, "/file", fi)
   690  	if err != nil {
   691  		t.Fatalf("findETag /file failed: %v", err)
   692  	}
   693  	matchETag := regexp.MustCompile(`^"-?[0-9a-f]{6,}"$`)
   694  	if !matchETag.MatchString(originalETag) {
   695  		t.Fatalf("ETag wrong, wanted something matching %v got %q", matchETag, originalETag)
   696  	}
   697  
   698  	// Now try overriding the ETag
   699  	o := &overrideETag{fi, `"OverriddenETag"`, nil}
   700  	ETag, err := findETag(ctx, fs, nil, "/file", o)
   701  	if err != nil {
   702  		t.Fatalf("findETag /file failed: %v", err)
   703  	}
   704  	if ETag != o.eTag {
   705  		t.Fatalf("ETag wrong want %q got %q", o.eTag, ETag)
   706  	}
   707  
   708  	// Now return ErrNotImplemented and check we get the original Etag
   709  	o = &overrideETag{fi, `"OverriddenETag"`, ErrNotImplemented}
   710  	ETag, err = findETag(ctx, fs, nil, "/file", o)
   711  	if err != nil {
   712  		t.Fatalf("findETag /file failed: %v", err)
   713  	}
   714  	if ETag != originalETag {
   715  		t.Fatalf("ETag wrong want %q got %q", originalETag, ETag)
   716  	}
   717  }