1
2
3
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
22
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,
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,
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,
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 {Space: "DAV:", Local: "resourcetype"},
153 {Space: "foo", Local: "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,
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,
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{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "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{{Space: "DAV:", Local: "getetag"}},
252 wantPropstats: []Propstat{{
253 Status: http.StatusOK,
254 Props: []Property{{
255 XMLName: xml.Name{Space: "DAV:", Local: "getetag"},
256 InnerXML: nil,
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{{Space: "foo:", Local: "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
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
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
603
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
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
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
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
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
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
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
View as plain text