You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

613 lines
16 KiB

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