prop_test.go 16 KB

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