golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/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 {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, // 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{{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, // 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{{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 // 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 }