prop_test.go 19 KB

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