prop_test.go 16 KB

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