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