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
5package webdav
6
7import (
8	"context"
9	"encoding/xml"
10	"fmt"
11	"net/http"
12	"os"
13	"reflect"
14	"regexp"
15	"sort"
16	"testing"
17)
18
19func 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
565func 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
572type byXMLName []xml.Name
573
574func (b byXMLName) Len() int           { return len(b) }
575func (b byXMLName) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
576func (b byXMLName) Less(i, j int) bool { return cmpXMLName(b[i], b[j]) }
577
578type byPropname []Property
579
580func (b byPropname) Len() int           { return len(b) }
581func (b byPropname) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
582func (b byPropname) Less(i, j int) bool { return cmpXMLName(b[i].XMLName, b[j].XMLName) }
583
584type byStatus []Propstat
585
586func (b byStatus) Len() int           { return len(b) }
587func (b byStatus) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
588func (b byStatus) Less(i, j int) bool { return b[i].Status < b[j].Status }
589
590type noDeadPropsFS struct {
591	FileSystem
592}
593
594func (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.
604type noDeadPropsFile struct {
605	f File
606}
607
608func (f noDeadPropsFile) Close() error                              { return f.f.Close() }
609func (f noDeadPropsFile) Read(p []byte) (int, error)                { return f.f.Read(p) }
610func (f noDeadPropsFile) Readdir(count int) ([]os.FileInfo, error)  { return f.f.Readdir(count) }
611func (f noDeadPropsFile) Seek(off int64, whence int) (int64, error) { return f.f.Seek(off, whence) }
612func (f noDeadPropsFile) Stat() (os.FileInfo, error)                { return f.f.Stat() }
613func (f noDeadPropsFile) Write(p []byte) (int, error)               { return f.f.Write(p) }
614
615type overrideContentType struct {
616	os.FileInfo
617	contentType string
618	err         error
619}
620
621func (o *overrideContentType) ContentType(ctx context.Context) (string, error) {
622	return o.contentType, o.err
623}
624
625func 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
666type overrideETag struct {
667	os.FileInfo
668	eTag string
669	err  error
670}
671
672func (o *overrideETag) ETag(ctx context.Context) (string, error) {
673	return o.eTag, o.err
674}
675
676func 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}
717