prop_test.go 16 KB

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