xml_test.go 25 KB

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