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.

906 lines
25 KiB

  1. // Copyright 2014 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. "bytes"
  7. "encoding/xml"
  8. "fmt"
  9. "io"
  10. "net/http"
  11. "net/http/httptest"
  12. "reflect"
  13. "sort"
  14. "strings"
  15. "testing"
  16. ixml "golang.org/x/net/webdav/internal/xml"
  17. )
  18. func TestReadLockInfo(t *testing.T) {
  19. // The "section x.y.z" test cases come from section x.y.z of the spec at
  20. // http://www.webdav.org/specs/rfc4918.html
  21. testCases := []struct {
  22. desc string
  23. input string
  24. wantLI lockInfo
  25. wantStatus int
  26. }{{
  27. "bad: junk",
  28. "xxx",
  29. lockInfo{},
  30. http.StatusBadRequest,
  31. }, {
  32. "bad: invalid owner XML",
  33. "" +
  34. "<D:lockinfo xmlns:D='DAV:'>\n" +
  35. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  36. " <D:locktype><D:write/></D:locktype>\n" +
  37. " <D:owner>\n" +
  38. " <D:href> no end tag \n" +
  39. " </D:owner>\n" +
  40. "</D:lockinfo>",
  41. lockInfo{},
  42. http.StatusBadRequest,
  43. }, {
  44. "bad: invalid UTF-8",
  45. "" +
  46. "<D:lockinfo xmlns:D='DAV:'>\n" +
  47. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  48. " <D:locktype><D:write/></D:locktype>\n" +
  49. " <D:owner>\n" +
  50. " <D:href> \xff </D:href>\n" +
  51. " </D:owner>\n" +
  52. "</D:lockinfo>",
  53. lockInfo{},
  54. http.StatusBadRequest,
  55. }, {
  56. "bad: unfinished XML #1",
  57. "" +
  58. "<D:lockinfo xmlns:D='DAV:'>\n" +
  59. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  60. " <D:locktype><D:write/></D:locktype>\n",
  61. lockInfo{},
  62. http.StatusBadRequest,
  63. }, {
  64. "bad: unfinished XML #2",
  65. "" +
  66. "<D:lockinfo xmlns:D='DAV:'>\n" +
  67. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  68. " <D:locktype><D:write/></D:locktype>\n" +
  69. " <D:owner>\n",
  70. lockInfo{},
  71. http.StatusBadRequest,
  72. }, {
  73. "good: empty",
  74. "",
  75. lockInfo{},
  76. 0,
  77. }, {
  78. "good: plain-text owner",
  79. "" +
  80. "<D:lockinfo xmlns:D='DAV:'>\n" +
  81. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  82. " <D:locktype><D:write/></D:locktype>\n" +
  83. " <D:owner>gopher</D:owner>\n" +
  84. "</D:lockinfo>",
  85. lockInfo{
  86. XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"},
  87. Exclusive: new(struct{}),
  88. Write: new(struct{}),
  89. Owner: owner{
  90. InnerXML: "gopher",
  91. },
  92. },
  93. 0,
  94. }, {
  95. "section 9.10.7",
  96. "" +
  97. "<D:lockinfo xmlns:D='DAV:'>\n" +
  98. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  99. " <D:locktype><D:write/></D:locktype>\n" +
  100. " <D:owner>\n" +
  101. " <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
  102. " </D:owner>\n" +
  103. "</D:lockinfo>",
  104. lockInfo{
  105. XMLName: ixml.Name{Space: "DAV:", Local: "lockinfo"},
  106. Exclusive: new(struct{}),
  107. Write: new(struct{}),
  108. Owner: owner{
  109. InnerXML: "\n <D:href>http://example.org/~ejw/contact.html</D:href>\n ",
  110. },
  111. },
  112. 0,
  113. }}
  114. for _, tc := range testCases {
  115. li, status, err := readLockInfo(strings.NewReader(tc.input))
  116. if tc.wantStatus != 0 {
  117. if err == nil {
  118. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  119. continue
  120. }
  121. } else if err != nil {
  122. t.Errorf("%s: %v", tc.desc, err)
  123. continue
  124. }
  125. if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
  126. t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
  127. tc.desc, li, status, tc.wantLI, tc.wantStatus)
  128. continue
  129. }
  130. }
  131. }
  132. func TestReadPropfind(t *testing.T) {
  133. testCases := []struct {
  134. desc string
  135. input string
  136. wantPF propfind
  137. wantStatus int
  138. }{{
  139. desc: "propfind: propname",
  140. input: "" +
  141. "<A:propfind xmlns:A='DAV:'>\n" +
  142. " <A:propname/>\n" +
  143. "</A:propfind>",
  144. wantPF: propfind{
  145. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  146. Propname: new(struct{}),
  147. },
  148. }, {
  149. desc: "propfind: empty body means allprop",
  150. input: "",
  151. wantPF: propfind{
  152. Allprop: new(struct{}),
  153. },
  154. }, {
  155. desc: "propfind: allprop",
  156. input: "" +
  157. "<A:propfind xmlns:A='DAV:'>\n" +
  158. " <A:allprop/>\n" +
  159. "</A:propfind>",
  160. wantPF: propfind{
  161. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  162. Allprop: new(struct{}),
  163. },
  164. }, {
  165. desc: "propfind: allprop followed by include",
  166. input: "" +
  167. "<A:propfind xmlns:A='DAV:'>\n" +
  168. " <A:allprop/>\n" +
  169. " <A:include><A:displayname/></A:include>\n" +
  170. "</A:propfind>",
  171. wantPF: propfind{
  172. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  173. Allprop: new(struct{}),
  174. Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  175. },
  176. }, {
  177. desc: "propfind: include followed by allprop",
  178. input: "" +
  179. "<A:propfind xmlns:A='DAV:'>\n" +
  180. " <A:include><A:displayname/></A:include>\n" +
  181. " <A:allprop/>\n" +
  182. "</A:propfind>",
  183. wantPF: propfind{
  184. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  185. Allprop: new(struct{}),
  186. Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  187. },
  188. }, {
  189. desc: "propfind: propfind",
  190. input: "" +
  191. "<A:propfind xmlns:A='DAV:'>\n" +
  192. " <A:prop><A:displayname/></A:prop>\n" +
  193. "</A:propfind>",
  194. wantPF: propfind{
  195. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  196. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  197. },
  198. }, {
  199. desc: "propfind: prop with ignored comments",
  200. input: "" +
  201. "<A:propfind xmlns:A='DAV:'>\n" +
  202. " <A:prop>\n" +
  203. " <!-- ignore -->\n" +
  204. " <A:displayname><!-- ignore --></A:displayname>\n" +
  205. " </A:prop>\n" +
  206. "</A:propfind>",
  207. wantPF: propfind{
  208. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  209. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  210. },
  211. }, {
  212. desc: "propfind: propfind with ignored whitespace",
  213. input: "" +
  214. "<A:propfind xmlns:A='DAV:'>\n" +
  215. " <A:prop> <A:displayname/></A:prop>\n" +
  216. "</A:propfind>",
  217. wantPF: propfind{
  218. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  219. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  220. },
  221. }, {
  222. desc: "propfind: propfind with ignored mixed-content",
  223. input: "" +
  224. "<A:propfind xmlns:A='DAV:'>\n" +
  225. " <A:prop>foo<A:displayname/>bar</A:prop>\n" +
  226. "</A:propfind>",
  227. wantPF: propfind{
  228. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  229. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  230. },
  231. }, {
  232. desc: "propfind: propname with ignored element (section A.4)",
  233. input: "" +
  234. "<A:propfind xmlns:A='DAV:'>\n" +
  235. " <A:propname/>\n" +
  236. " <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
  237. "</A:propfind>",
  238. wantPF: propfind{
  239. XMLName: ixml.Name{Space: "DAV:", Local: "propfind"},
  240. Propname: new(struct{}),
  241. },
  242. }, {
  243. desc: "propfind: bad: junk",
  244. input: "xxx",
  245. wantStatus: http.StatusBadRequest,
  246. }, {
  247. desc: "propfind: bad: propname and allprop (section A.3)",
  248. input: "" +
  249. "<A:propfind xmlns:A='DAV:'>\n" +
  250. " <A:propname/>" +
  251. " <A:allprop/>" +
  252. "</A:propfind>",
  253. wantStatus: http.StatusBadRequest,
  254. }, {
  255. desc: "propfind: bad: propname and prop",
  256. input: "" +
  257. "<A:propfind xmlns:A='DAV:'>\n" +
  258. " <A:prop><A:displayname/></A:prop>\n" +
  259. " <A:propname/>\n" +
  260. "</A:propfind>",
  261. wantStatus: http.StatusBadRequest,
  262. }, {
  263. desc: "propfind: bad: allprop and prop",
  264. input: "" +
  265. "<A:propfind xmlns:A='DAV:'>\n" +
  266. " <A:allprop/>\n" +
  267. " <A:prop><A:foo/><A:/prop>\n" +
  268. "</A:propfind>",
  269. wantStatus: http.StatusBadRequest,
  270. }, {
  271. desc: "propfind: bad: empty propfind with ignored element (section A.4)",
  272. input: "" +
  273. "<A:propfind xmlns:A='DAV:'>\n" +
  274. " <E:expired-props/>\n" +
  275. "</A:propfind>",
  276. wantStatus: http.StatusBadRequest,
  277. }, {
  278. desc: "propfind: bad: empty prop",
  279. input: "" +
  280. "<A:propfind xmlns:A='DAV:'>\n" +
  281. " <A:prop/>\n" +
  282. "</A:propfind>",
  283. wantStatus: http.StatusBadRequest,
  284. }, {
  285. desc: "propfind: bad: prop with just chardata",
  286. input: "" +
  287. "<A:propfind xmlns:A='DAV:'>\n" +
  288. " <A:prop>foo</A:prop>\n" +
  289. "</A:propfind>",
  290. wantStatus: http.StatusBadRequest,
  291. }, {
  292. desc: "bad: interrupted prop",
  293. input: "" +
  294. "<A:propfind xmlns:A='DAV:'>\n" +
  295. " <A:prop><A:foo></A:prop>\n",
  296. wantStatus: http.StatusBadRequest,
  297. }, {
  298. desc: "bad: malformed end element prop",
  299. input: "" +
  300. "<A:propfind xmlns:A='DAV:'>\n" +
  301. " <A:prop><A:foo/></A:bar></A:prop>\n",
  302. wantStatus: http.StatusBadRequest,
  303. }, {
  304. desc: "propfind: bad: property with chardata value",
  305. input: "" +
  306. "<A:propfind xmlns:A='DAV:'>\n" +
  307. " <A:prop><A:foo>bar</A:foo></A:prop>\n" +
  308. "</A:propfind>",
  309. wantStatus: http.StatusBadRequest,
  310. }, {
  311. desc: "propfind: bad: property with whitespace value",
  312. input: "" +
  313. "<A:propfind xmlns:A='DAV:'>\n" +
  314. " <A:prop><A:foo> </A:foo></A:prop>\n" +
  315. "</A:propfind>",
  316. wantStatus: http.StatusBadRequest,
  317. }, {
  318. desc: "propfind: bad: include without allprop",
  319. input: "" +
  320. "<A:propfind xmlns:A='DAV:'>\n" +
  321. " <A:include><A:foo/></A:include>\n" +
  322. "</A:propfind>",
  323. wantStatus: http.StatusBadRequest,
  324. }}
  325. for _, tc := range testCases {
  326. pf, status, err := readPropfind(strings.NewReader(tc.input))
  327. if tc.wantStatus != 0 {
  328. if err == nil {
  329. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  330. continue
  331. }
  332. } else if err != nil {
  333. t.Errorf("%s: %v", tc.desc, err)
  334. continue
  335. }
  336. if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
  337. t.Errorf("%s:\ngot propfind=%v, status=%v\nwant propfind=%v, status=%v",
  338. tc.desc, pf, status, tc.wantPF, tc.wantStatus)
  339. continue
  340. }
  341. }
  342. }
  343. func TestMultistatusWriter(t *testing.T) {
  344. ///The "section x.y.z" test cases come from section x.y.z of the spec at
  345. // http://www.webdav.org/specs/rfc4918.html
  346. testCases := []struct {
  347. desc string
  348. responses []response
  349. respdesc string
  350. writeHeader bool
  351. wantXML string
  352. wantCode int
  353. wantErr error
  354. }{{
  355. desc: "section 9.2.2 (failed dependency)",
  356. responses: []response{{
  357. Href: []string{"http://example.com/foo"},
  358. Propstat: []propstat{{
  359. Prop: []Property{{
  360. XMLName: xml.Name{
  361. Space: "http://ns.example.com/",
  362. Local: "Authors",
  363. },
  364. }},
  365. Status: "HTTP/1.1 424 Failed Dependency",
  366. }, {
  367. Prop: []Property{{
  368. XMLName: xml.Name{
  369. Space: "http://ns.example.com/",
  370. Local: "Copyright-Owner",
  371. },
  372. }},
  373. Status: "HTTP/1.1 409 Conflict",
  374. }},
  375. ResponseDescription: "Copyright Owner cannot be deleted or altered.",
  376. }},
  377. wantXML: `` +
  378. `<?xml version="1.0" encoding="UTF-8"?>` +
  379. `<multistatus xmlns="DAV:">` +
  380. ` <response>` +
  381. ` <href>http://example.com/foo</href>` +
  382. ` <propstat>` +
  383. ` <prop>` +
  384. ` <Authors xmlns="http://ns.example.com/"></Authors>` +
  385. ` </prop>` +
  386. ` <status>HTTP/1.1 424 Failed Dependency</status>` +
  387. ` </propstat>` +
  388. ` <propstat xmlns="DAV:">` +
  389. ` <prop>` +
  390. ` <Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
  391. ` </prop>` +
  392. ` <status>HTTP/1.1 409 Conflict</status>` +
  393. ` </propstat>` +
  394. ` <responsedescription>Copyright Owner cannot be deleted or altered.</responsedescription>` +
  395. `</response>` +
  396. `</multistatus>`,
  397. wantCode: StatusMulti,
  398. }, {
  399. desc: "section 9.6.2 (lock-token-submitted)",
  400. responses: []response{{
  401. Href: []string{"http://example.com/foo"},
  402. Status: "HTTP/1.1 423 Locked",
  403. Error: &xmlError{
  404. InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
  405. },
  406. }},
  407. wantXML: `` +
  408. `<?xml version="1.0" encoding="UTF-8"?>` +
  409. `<multistatus xmlns="DAV:">` +
  410. ` <response>` +
  411. ` <href>http://example.com/foo</href>` +
  412. ` <status>HTTP/1.1 423 Locked</status>` +
  413. ` <error><lock-token-submitted xmlns="DAV:"/></error>` +
  414. ` </response>` +
  415. `</multistatus>`,
  416. wantCode: StatusMulti,
  417. }, {
  418. desc: "section 9.1.3",
  419. responses: []response{{
  420. Href: []string{"http://example.com/foo"},
  421. Propstat: []propstat{{
  422. Prop: []Property{{
  423. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
  424. InnerXML: []byte(`` +
  425. `<BoxType xmlns="http://ns.example.com/boxschema/">` +
  426. `Box type A` +
  427. `</BoxType>`),
  428. }, {
  429. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
  430. InnerXML: []byte(`` +
  431. `<Name xmlns="http://ns.example.com/boxschema/">` +
  432. `J.J. Johnson` +
  433. `</Name>`),
  434. }},
  435. Status: "HTTP/1.1 200 OK",
  436. }, {
  437. Prop: []Property{{
  438. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
  439. }, {
  440. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
  441. }},
  442. Status: "HTTP/1.1 403 Forbidden",
  443. ResponseDescription: "The user does not have access to the DingALing property.",
  444. }},
  445. }},
  446. respdesc: "There has been an access violation error.",
  447. wantXML: `` +
  448. `<?xml version="1.0" encoding="UTF-8"?>` +
  449. `<multistatus xmlns="DAV:" xmlns:B="http://ns.example.com/boxschema/">` +
  450. ` <response>` +
  451. ` <href>http://example.com/foo</href>` +
  452. ` <propstat>` +
  453. ` <prop>` +
  454. ` <B:bigbox><B:BoxType>Box type A</B:BoxType></B:bigbox>` +
  455. ` <B:author><B:Name>J.J. Johnson</B:Name></B:author>` +
  456. ` </prop>` +
  457. ` <status>HTTP/1.1 200 OK</status>` +
  458. ` </propstat>` +
  459. ` <propstat>` +
  460. ` <prop>` +
  461. ` <B:DingALing/>` +
  462. ` <B:Random/>` +
  463. ` </prop>` +
  464. ` <status>HTTP/1.1 403 Forbidden</status>` +
  465. ` <responsedescription>The user does not have access to the DingALing property.</responsedescription>` +
  466. ` </propstat>` +
  467. ` </response>` +
  468. ` <responsedescription>There has been an access violation error.</responsedescription>` +
  469. `</multistatus>`,
  470. wantCode: StatusMulti,
  471. }, {
  472. desc: "no response written",
  473. // default of http.responseWriter
  474. wantCode: http.StatusOK,
  475. }, {
  476. desc: "no response written (with description)",
  477. respdesc: "too bad",
  478. // default of http.responseWriter
  479. wantCode: http.StatusOK,
  480. }, {
  481. desc: "empty multistatus with header",
  482. writeHeader: true,
  483. wantXML: `<multistatus xmlns="DAV:"></multistatus>`,
  484. wantCode: StatusMulti,
  485. }, {
  486. desc: "bad: no href",
  487. responses: []response{{
  488. Propstat: []propstat{{
  489. Prop: []Property{{
  490. XMLName: xml.Name{
  491. Space: "http://example.com/",
  492. Local: "foo",
  493. },
  494. }},
  495. Status: "HTTP/1.1 200 OK",
  496. }},
  497. }},
  498. wantErr: errInvalidResponse,
  499. // default of http.responseWriter
  500. wantCode: http.StatusOK,
  501. }, {
  502. desc: "bad: multiple hrefs and no status",
  503. responses: []response{{
  504. Href: []string{"http://example.com/foo", "http://example.com/bar"},
  505. }},
  506. wantErr: errInvalidResponse,
  507. // default of http.responseWriter
  508. wantCode: http.StatusOK,
  509. }, {
  510. desc: "bad: one href and no propstat",
  511. responses: []response{{
  512. Href: []string{"http://example.com/foo"},
  513. }},
  514. wantErr: errInvalidResponse,
  515. // default of http.responseWriter
  516. wantCode: http.StatusOK,
  517. }, {
  518. desc: "bad: status with one href and propstat",
  519. responses: []response{{
  520. Href: []string{"http://example.com/foo"},
  521. Propstat: []propstat{{
  522. Prop: []Property{{
  523. XMLName: xml.Name{
  524. Space: "http://example.com/",
  525. Local: "foo",
  526. },
  527. }},
  528. Status: "HTTP/1.1 200 OK",
  529. }},
  530. Status: "HTTP/1.1 200 OK",
  531. }},
  532. wantErr: errInvalidResponse,
  533. // default of http.responseWriter
  534. wantCode: http.StatusOK,
  535. }, {
  536. desc: "bad: multiple hrefs and propstat",
  537. responses: []response{{
  538. Href: []string{
  539. "http://example.com/foo",
  540. "http://example.com/bar",
  541. },
  542. Propstat: []propstat{{
  543. Prop: []Property{{
  544. XMLName: xml.Name{
  545. Space: "http://example.com/",
  546. Local: "foo",
  547. },
  548. }},
  549. Status: "HTTP/1.1 200 OK",
  550. }},
  551. }},
  552. wantErr: errInvalidResponse,
  553. // default of http.responseWriter
  554. wantCode: http.StatusOK,
  555. }}
  556. n := xmlNormalizer{omitWhitespace: true}
  557. loop:
  558. for _, tc := range testCases {
  559. rec := httptest.NewRecorder()
  560. w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
  561. if tc.writeHeader {
  562. if err := w.writeHeader(); err != nil {
  563. t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
  564. continue
  565. }
  566. }
  567. for _, r := range tc.responses {
  568. if err := w.write(&r); err != nil {
  569. if err != tc.wantErr {
  570. t.Errorf("%s: got write error %v, want %v",
  571. tc.desc, err, tc.wantErr)
  572. }
  573. continue loop
  574. }
  575. }
  576. if err := w.close(); err != tc.wantErr {
  577. t.Errorf("%s: got close error %v, want %v",
  578. tc.desc, err, tc.wantErr)
  579. continue
  580. }
  581. if rec.Code != tc.wantCode {
  582. t.Errorf("%s: got HTTP status code %d, want %d\n",
  583. tc.desc, rec.Code, tc.wantCode)
  584. continue
  585. }
  586. gotXML := rec.Body.String()
  587. eq, err := n.equalXML(strings.NewReader(gotXML), strings.NewReader(tc.wantXML))
  588. if err != nil {
  589. t.Errorf("%s: equalXML: %v", tc.desc, err)
  590. continue
  591. }
  592. if !eq {
  593. t.Errorf("%s: XML body\ngot %s\nwant %s", tc.desc, gotXML, tc.wantXML)
  594. }
  595. }
  596. }
  597. func TestReadProppatch(t *testing.T) {
  598. ppStr := func(pps []Proppatch) string {
  599. var outer []string
  600. for _, pp := range pps {
  601. var inner []string
  602. for _, p := range pp.Props {
  603. inner = append(inner, fmt.Sprintf("{XMLName: %q, Lang: %q, InnerXML: %q}",
  604. p.XMLName, p.Lang, p.InnerXML))
  605. }
  606. outer = append(outer, fmt.Sprintf("{Remove: %t, Props: [%s]}",
  607. pp.Remove, strings.Join(inner, ", ")))
  608. }
  609. return "[" + strings.Join(outer, ", ") + "]"
  610. }
  611. testCases := []struct {
  612. desc string
  613. input string
  614. wantPP []Proppatch
  615. wantStatus int
  616. }{{
  617. desc: "proppatch: section 9.2 (with simple property value)",
  618. input: `` +
  619. `<?xml version="1.0" encoding="utf-8" ?>` +
  620. `<D:propertyupdate xmlns:D="DAV:"` +
  621. ` xmlns:Z="http://ns.example.com/z/">` +
  622. ` <D:set>` +
  623. ` <D:prop><Z:Authors>somevalue</Z:Authors></D:prop>` +
  624. ` </D:set>` +
  625. ` <D:remove>` +
  626. ` <D:prop><Z:Copyright-Owner/></D:prop>` +
  627. ` </D:remove>` +
  628. `</D:propertyupdate>`,
  629. wantPP: []Proppatch{{
  630. Props: []Property{{
  631. xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
  632. "",
  633. []byte(`somevalue`),
  634. }},
  635. }, {
  636. Remove: true,
  637. Props: []Property{{
  638. xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
  639. "",
  640. nil,
  641. }},
  642. }},
  643. }, {
  644. desc: "proppatch: lang attribute on prop",
  645. input: `` +
  646. `<?xml version="1.0" encoding="utf-8" ?>` +
  647. `<D:propertyupdate xmlns:D="DAV:">` +
  648. ` <D:set>` +
  649. ` <D:prop xml:lang="en">` +
  650. ` <foo xmlns="http://example.com/ns"/>` +
  651. ` </D:prop>` +
  652. ` </D:set>` +
  653. `</D:propertyupdate>`,
  654. wantPP: []Proppatch{{
  655. Props: []Property{{
  656. xml.Name{Space: "http://example.com/ns", Local: "foo"},
  657. "en",
  658. nil,
  659. }},
  660. }},
  661. }, {
  662. desc: "bad: remove with value",
  663. input: `` +
  664. `<?xml version="1.0" encoding="utf-8" ?>` +
  665. `<D:propertyupdate xmlns:D="DAV:"` +
  666. ` xmlns:Z="http://ns.example.com/z/">` +
  667. ` <D:remove>` +
  668. ` <D:prop>` +
  669. ` <Z:Authors>` +
  670. ` <Z:Author>Jim Whitehead</Z:Author>` +
  671. ` </Z:Authors>` +
  672. ` </D:prop>` +
  673. ` </D:remove>` +
  674. `</D:propertyupdate>`,
  675. wantStatus: http.StatusBadRequest,
  676. }, {
  677. desc: "bad: empty propertyupdate",
  678. input: `` +
  679. `<?xml version="1.0" encoding="utf-8" ?>` +
  680. `<D:propertyupdate xmlns:D="DAV:"` +
  681. `</D:propertyupdate>`,
  682. wantStatus: http.StatusBadRequest,
  683. }, {
  684. desc: "bad: empty prop",
  685. input: `` +
  686. `<?xml version="1.0" encoding="utf-8" ?>` +
  687. `<D:propertyupdate xmlns:D="DAV:"` +
  688. ` xmlns:Z="http://ns.example.com/z/">` +
  689. ` <D:remove>` +
  690. ` <D:prop/>` +
  691. ` </D:remove>` +
  692. `</D:propertyupdate>`,
  693. wantStatus: http.StatusBadRequest,
  694. }}
  695. for _, tc := range testCases {
  696. pp, status, err := readProppatch(strings.NewReader(tc.input))
  697. if tc.wantStatus != 0 {
  698. if err == nil {
  699. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  700. continue
  701. }
  702. } else if err != nil {
  703. t.Errorf("%s: %v", tc.desc, err)
  704. continue
  705. }
  706. if status != tc.wantStatus {
  707. t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
  708. continue
  709. }
  710. if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
  711. t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, ppStr(pp), ppStr(tc.wantPP))
  712. }
  713. }
  714. }
  715. func TestUnmarshalXMLValue(t *testing.T) {
  716. testCases := []struct {
  717. desc string
  718. input string
  719. wantVal string
  720. }{{
  721. desc: "simple char data",
  722. input: "<root>foo</root>",
  723. wantVal: "foo",
  724. }, {
  725. desc: "empty element",
  726. input: "<root><foo/></root>",
  727. wantVal: "<foo/>",
  728. }, {
  729. desc: "preserve namespace",
  730. input: `<root><foo xmlns="bar"/></root>`,
  731. wantVal: `<foo xmlns="bar"/>`,
  732. }, {
  733. desc: "preserve root element namespace",
  734. input: `<root xmlns:bar="bar"><bar:foo/></root>`,
  735. wantVal: `<foo xmlns="bar"/>`,
  736. }, {
  737. desc: "preserve whitespace",
  738. input: "<root> \t </root>",
  739. wantVal: " \t ",
  740. }, {
  741. desc: "preserve mixed content",
  742. input: `<root xmlns="bar"> <foo>a<bam xmlns="baz"/> </foo> </root>`,
  743. wantVal: ` <foo xmlns="bar">a<bam xmlns="baz"/> </foo> `,
  744. }, {
  745. desc: "section 9.2",
  746. input: `` +
  747. `<Z:Authors xmlns:Z="http://ns.example.com/z/">` +
  748. ` <Z:Author>Jim Whitehead</Z:Author>` +
  749. ` <Z:Author>Roy Fielding</Z:Author>` +
  750. `</Z:Authors>`,
  751. wantVal: `` +
  752. ` <Author xmlns="http://ns.example.com/z/">Jim Whitehead</Author>` +
  753. ` <Author xmlns="http://ns.example.com/z/">Roy Fielding</Author>`,
  754. }, {
  755. desc: "section 4.3.1 (mixed content)",
  756. input: `` +
  757. `<x:author ` +
  758. ` xmlns:x='http://example.com/ns' ` +
  759. ` xmlns:D="DAV:">` +
  760. ` <x:name>Jane Doe</x:name>` +
  761. ` <!-- Jane's contact info -->` +
  762. ` <x:uri type='email'` +
  763. ` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
  764. ` <x:uri type='web'` +
  765. ` added='2005-11-27'>http://www.example.com</x:uri>` +
  766. ` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
  767. ` Jane has been working way <h:em>too</h:em> long on the` +
  768. ` long-awaited revision of <![CDATA[<RFC2518>]]>.` +
  769. ` </x:notes>` +
  770. `</x:author>`,
  771. wantVal: `` +
  772. ` <name xmlns="http://example.com/ns">Jane Doe</name>` +
  773. ` ` +
  774. ` <uri type='email'` +
  775. ` xmlns="http://example.com/ns" ` +
  776. ` added='2005-11-26'>mailto:jane.doe@example.com</uri>` +
  777. ` <uri added='2005-11-27'` +
  778. ` type='web'` +
  779. ` xmlns="http://example.com/ns">http://www.example.com</uri>` +
  780. ` <notes xmlns="http://example.com/ns" ` +
  781. ` xmlns:h="http://www.w3.org/1999/xhtml">` +
  782. ` Jane has been working way <h:em>too</h:em> long on the` +
  783. ` long-awaited revision of &lt;RFC2518&gt;.` +
  784. ` </notes>`,
  785. }}
  786. var n xmlNormalizer
  787. for _, tc := range testCases {
  788. d := ixml.NewDecoder(strings.NewReader(tc.input))
  789. var v xmlValue
  790. if err := d.Decode(&v); err != nil {
  791. t.Errorf("%s: got error %v, want nil", tc.desc, err)
  792. continue
  793. }
  794. eq, err := n.equalXML(bytes.NewReader(v), strings.NewReader(tc.wantVal))
  795. if err != nil {
  796. t.Errorf("%s: equalXML: %v", tc.desc, err)
  797. continue
  798. }
  799. if !eq {
  800. t.Errorf("%s:\ngot %s\nwant %s", tc.desc, string(v), tc.wantVal)
  801. }
  802. }
  803. }
  804. // xmlNormalizer normalizes XML.
  805. type xmlNormalizer struct {
  806. // omitWhitespace instructs to ignore whitespace between element tags.
  807. omitWhitespace bool
  808. // omitComments instructs to ignore XML comments.
  809. omitComments bool
  810. }
  811. // normalize writes the normalized XML content of r to w. It applies the
  812. // following rules
  813. //
  814. // * Rename namespace prefixes according to an internal heuristic.
  815. // * Remove unnecessary namespace declarations.
  816. // * Sort attributes in XML start elements in lexical order of their
  817. // fully qualified name.
  818. // * Remove XML directives and processing instructions.
  819. // * Remove CDATA between XML tags that only contains whitespace, if
  820. // instructed to do so.
  821. // * Remove comments, if instructed to do so.
  822. //
  823. func (n *xmlNormalizer) normalize(w io.Writer, r io.Reader) error {
  824. d := ixml.NewDecoder(r)
  825. e := ixml.NewEncoder(w)
  826. for {
  827. t, err := d.Token()
  828. if err != nil {
  829. if t == nil && err == io.EOF {
  830. break
  831. }
  832. return err
  833. }
  834. switch val := t.(type) {
  835. case ixml.Directive, ixml.ProcInst:
  836. continue
  837. case ixml.Comment:
  838. if n.omitComments {
  839. continue
  840. }
  841. case ixml.CharData:
  842. if n.omitWhitespace && len(bytes.TrimSpace(val)) == 0 {
  843. continue
  844. }
  845. case ixml.StartElement:
  846. start, _ := ixml.CopyToken(val).(ixml.StartElement)
  847. attr := start.Attr[:0]
  848. for _, a := range start.Attr {
  849. if a.Name.Space == "xmlns" || a.Name.Local == "xmlns" {
  850. continue
  851. }
  852. attr = append(attr, a)
  853. }
  854. sort.Sort(byName(attr))
  855. start.Attr = attr
  856. t = start
  857. }
  858. err = e.EncodeToken(t)
  859. if err != nil {
  860. return err
  861. }
  862. }
  863. return e.Flush()
  864. }
  865. // equalXML tests for equality of the normalized XML contents of a and b.
  866. func (n *xmlNormalizer) equalXML(a, b io.Reader) (bool, error) {
  867. var buf bytes.Buffer
  868. if err := n.normalize(&buf, a); err != nil {
  869. return false, err
  870. }
  871. normA := buf.String()
  872. buf.Reset()
  873. if err := n.normalize(&buf, b); err != nil {
  874. return false, err
  875. }
  876. normB := buf.String()
  877. return normA == normB, nil
  878. }
  879. type byName []ixml.Attr
  880. func (a byName) Len() int { return len(a) }
  881. func (a byName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
  882. func (a byName) Less(i, j int) bool {
  883. if a[i].Name.Space != a[j].Name.Space {
  884. return a[i].Name.Space < a[j].Name.Space
  885. }
  886. return a[i].Name.Local < a[j].Name.Local
  887. }