xml_test.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812
  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. "io"
  9. "net/http"
  10. "net/http/httptest"
  11. "reflect"
  12. "strings"
  13. "testing"
  14. )
  15. func TestReadLockInfo(t *testing.T) {
  16. // The "section x.y.z" test cases come from section x.y.z of the spec at
  17. // http://www.webdav.org/specs/rfc4918.html
  18. testCases := []struct {
  19. desc string
  20. input string
  21. wantLI lockInfo
  22. wantStatus int
  23. }{{
  24. "bad: junk",
  25. "xxx",
  26. lockInfo{},
  27. http.StatusBadRequest,
  28. }, {
  29. "bad: invalid owner XML",
  30. "" +
  31. "<D:lockinfo xmlns:D='DAV:'>\n" +
  32. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  33. " <D:locktype><D:write/></D:locktype>\n" +
  34. " <D:owner>\n" +
  35. " <D:href> no end tag \n" +
  36. " </D:owner>\n" +
  37. "</D:lockinfo>",
  38. lockInfo{},
  39. http.StatusBadRequest,
  40. }, {
  41. "bad: invalid UTF-8",
  42. "" +
  43. "<D:lockinfo xmlns:D='DAV:'>\n" +
  44. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  45. " <D:locktype><D:write/></D:locktype>\n" +
  46. " <D:owner>\n" +
  47. " <D:href> \xff </D:href>\n" +
  48. " </D:owner>\n" +
  49. "</D:lockinfo>",
  50. lockInfo{},
  51. http.StatusBadRequest,
  52. }, {
  53. "bad: unfinished XML #1",
  54. "" +
  55. "<D:lockinfo xmlns:D='DAV:'>\n" +
  56. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  57. " <D:locktype><D:write/></D:locktype>\n",
  58. lockInfo{},
  59. http.StatusBadRequest,
  60. }, {
  61. "bad: unfinished XML #2",
  62. "" +
  63. "<D:lockinfo xmlns:D='DAV:'>\n" +
  64. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  65. " <D:locktype><D:write/></D:locktype>\n" +
  66. " <D:owner>\n",
  67. lockInfo{},
  68. http.StatusBadRequest,
  69. }, {
  70. "good: empty",
  71. "",
  72. lockInfo{},
  73. 0,
  74. }, {
  75. "good: plain-text owner",
  76. "" +
  77. "<D:lockinfo xmlns:D='DAV:'>\n" +
  78. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  79. " <D:locktype><D:write/></D:locktype>\n" +
  80. " <D:owner>gopher</D:owner>\n" +
  81. "</D:lockinfo>",
  82. lockInfo{
  83. XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"},
  84. Exclusive: new(struct{}),
  85. Write: new(struct{}),
  86. Owner: owner{
  87. InnerXML: "gopher",
  88. },
  89. },
  90. 0,
  91. }, {
  92. "section 9.10.7",
  93. "" +
  94. "<D:lockinfo xmlns:D='DAV:'>\n" +
  95. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  96. " <D:locktype><D:write/></D:locktype>\n" +
  97. " <D:owner>\n" +
  98. " <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
  99. " </D:owner>\n" +
  100. "</D:lockinfo>",
  101. lockInfo{
  102. XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"},
  103. Exclusive: new(struct{}),
  104. Write: new(struct{}),
  105. Owner: owner{
  106. InnerXML: "\n <D:href>http://example.org/~ejw/contact.html</D:href>\n ",
  107. },
  108. },
  109. 0,
  110. }}
  111. for _, tc := range testCases {
  112. li, status, err := readLockInfo(strings.NewReader(tc.input))
  113. if tc.wantStatus != 0 {
  114. if err == nil {
  115. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  116. continue
  117. }
  118. } else if err != nil {
  119. t.Errorf("%s: %v", tc.desc, err)
  120. continue
  121. }
  122. if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
  123. t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
  124. tc.desc, li, status, tc.wantLI, tc.wantStatus)
  125. continue
  126. }
  127. }
  128. }
  129. func TestReadPropfind(t *testing.T) {
  130. testCases := []struct {
  131. desc string
  132. input string
  133. wantPF propfind
  134. wantStatus int
  135. }{{
  136. desc: "propfind: propname",
  137. input: "" +
  138. "<A:propfind xmlns:A='DAV:'>\n" +
  139. " <A:propname/>\n" +
  140. "</A:propfind>",
  141. wantPF: propfind{
  142. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  143. Propname: new(struct{}),
  144. },
  145. }, {
  146. desc: "propfind: empty body means allprop",
  147. input: "",
  148. wantPF: propfind{
  149. Allprop: new(struct{}),
  150. },
  151. }, {
  152. desc: "propfind: allprop",
  153. input: "" +
  154. "<A:propfind xmlns:A='DAV:'>\n" +
  155. " <A:allprop/>\n" +
  156. "</A:propfind>",
  157. wantPF: propfind{
  158. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  159. Allprop: new(struct{}),
  160. },
  161. }, {
  162. desc: "propfind: allprop followed by include",
  163. input: "" +
  164. "<A:propfind xmlns:A='DAV:'>\n" +
  165. " <A:allprop/>\n" +
  166. " <A:include><A:displayname/></A:include>\n" +
  167. "</A:propfind>",
  168. wantPF: propfind{
  169. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  170. Allprop: new(struct{}),
  171. Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  172. },
  173. }, {
  174. desc: "propfind: include followed by allprop",
  175. input: "" +
  176. "<A:propfind xmlns:A='DAV:'>\n" +
  177. " <A:include><A:displayname/></A:include>\n" +
  178. " <A:allprop/>\n" +
  179. "</A:propfind>",
  180. wantPF: propfind{
  181. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  182. Allprop: new(struct{}),
  183. Include: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  184. },
  185. }, {
  186. desc: "propfind: propfind",
  187. input: "" +
  188. "<A:propfind xmlns:A='DAV:'>\n" +
  189. " <A:prop><A:displayname/></A:prop>\n" +
  190. "</A:propfind>",
  191. wantPF: propfind{
  192. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  193. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  194. },
  195. }, {
  196. desc: "propfind: prop with ignored comments",
  197. input: "" +
  198. "<A:propfind xmlns:A='DAV:'>\n" +
  199. " <A:prop>\n" +
  200. " <!-- ignore -->\n" +
  201. " <A:displayname><!-- ignore --></A:displayname>\n" +
  202. " </A:prop>\n" +
  203. "</A:propfind>",
  204. wantPF: propfind{
  205. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  206. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  207. },
  208. }, {
  209. desc: "propfind: propfind with ignored whitespace",
  210. input: "" +
  211. "<A:propfind xmlns:A='DAV:'>\n" +
  212. " <A:prop> <A:displayname/></A:prop>\n" +
  213. "</A:propfind>",
  214. wantPF: propfind{
  215. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  216. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  217. },
  218. }, {
  219. desc: "propfind: propfind with ignored mixed-content",
  220. input: "" +
  221. "<A:propfind xmlns:A='DAV:'>\n" +
  222. " <A:prop>foo<A:displayname/>bar</A:prop>\n" +
  223. "</A:propfind>",
  224. wantPF: propfind{
  225. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  226. Prop: propfindProps{xml.Name{Space: "DAV:", Local: "displayname"}},
  227. },
  228. }, {
  229. desc: "propfind: propname with ignored element (section A.4)",
  230. input: "" +
  231. "<A:propfind xmlns:A='DAV:'>\n" +
  232. " <A:propname/>\n" +
  233. " <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
  234. "</A:propfind>",
  235. wantPF: propfind{
  236. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  237. Propname: new(struct{}),
  238. },
  239. }, {
  240. desc: "propfind: bad: junk",
  241. input: "xxx",
  242. wantStatus: http.StatusBadRequest,
  243. }, {
  244. desc: "propfind: bad: propname and allprop (section A.3)",
  245. input: "" +
  246. "<A:propfind xmlns:A='DAV:'>\n" +
  247. " <A:propname/>" +
  248. " <A:allprop/>" +
  249. "</A:propfind>",
  250. wantStatus: http.StatusBadRequest,
  251. }, {
  252. desc: "propfind: bad: propname and prop",
  253. input: "" +
  254. "<A:propfind xmlns:A='DAV:'>\n" +
  255. " <A:prop><A:displayname/></A:prop>\n" +
  256. " <A:propname/>\n" +
  257. "</A:propfind>",
  258. wantStatus: http.StatusBadRequest,
  259. }, {
  260. desc: "propfind: bad: allprop and prop",
  261. input: "" +
  262. "<A:propfind xmlns:A='DAV:'>\n" +
  263. " <A:allprop/>\n" +
  264. " <A:prop><A:foo/><A:/prop>\n" +
  265. "</A:propfind>",
  266. wantStatus: http.StatusBadRequest,
  267. }, {
  268. desc: "propfind: bad: empty propfind with ignored element (section A.4)",
  269. input: "" +
  270. "<A:propfind xmlns:A='DAV:'>\n" +
  271. " <E:expired-props/>\n" +
  272. "</A:propfind>",
  273. wantStatus: http.StatusBadRequest,
  274. }, {
  275. desc: "propfind: bad: empty prop",
  276. input: "" +
  277. "<A:propfind xmlns:A='DAV:'>\n" +
  278. " <A:prop/>\n" +
  279. "</A:propfind>",
  280. wantStatus: http.StatusBadRequest,
  281. }, {
  282. desc: "propfind: bad: prop with just chardata",
  283. input: "" +
  284. "<A:propfind xmlns:A='DAV:'>\n" +
  285. " <A:prop>foo</A:prop>\n" +
  286. "</A:propfind>",
  287. wantStatus: http.StatusBadRequest,
  288. }, {
  289. desc: "bad: interrupted prop",
  290. input: "" +
  291. "<A:propfind xmlns:A='DAV:'>\n" +
  292. " <A:prop><A:foo></A:prop>\n",
  293. wantStatus: http.StatusBadRequest,
  294. }, {
  295. desc: "bad: malformed end element prop",
  296. input: "" +
  297. "<A:propfind xmlns:A='DAV:'>\n" +
  298. " <A:prop><A:foo/></A:bar></A:prop>\n",
  299. wantStatus: http.StatusBadRequest,
  300. }, {
  301. desc: "propfind: bad: property with chardata value",
  302. input: "" +
  303. "<A:propfind xmlns:A='DAV:'>\n" +
  304. " <A:prop><A:foo>bar</A:foo></A:prop>\n" +
  305. "</A:propfind>",
  306. wantStatus: http.StatusBadRequest,
  307. }, {
  308. desc: "propfind: bad: property with whitespace value",
  309. input: "" +
  310. "<A:propfind xmlns:A='DAV:'>\n" +
  311. " <A:prop><A:foo> </A:foo></A:prop>\n" +
  312. "</A:propfind>",
  313. wantStatus: http.StatusBadRequest,
  314. }, {
  315. desc: "propfind: bad: include without allprop",
  316. input: "" +
  317. "<A:propfind xmlns:A='DAV:'>\n" +
  318. " <A:include><A:foo/></A:include>\n" +
  319. "</A:propfind>",
  320. wantStatus: http.StatusBadRequest,
  321. }}
  322. for _, tc := range testCases {
  323. pf, status, err := readPropfind(strings.NewReader(tc.input))
  324. if tc.wantStatus != 0 {
  325. if err == nil {
  326. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  327. continue
  328. }
  329. } else if err != nil {
  330. t.Errorf("%s: %v", tc.desc, err)
  331. continue
  332. }
  333. if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
  334. t.Errorf("%s:\ngot propfind=%v, status=%v\nwant propfind=%v, status=%v",
  335. tc.desc, pf, status, tc.wantPF, tc.wantStatus)
  336. continue
  337. }
  338. }
  339. }
  340. func TestMultistatusWriter(t *testing.T) {
  341. if go1Dot4 {
  342. t.Skip("TestMultistatusWriter requires Go version 1.5 or greater")
  343. }
  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:">` +
  450. ` <response>` +
  451. ` <href>http://example.com/foo</href>` +
  452. ` <propstat>` +
  453. ` <prop>` +
  454. ` <bigbox xmlns="http://ns.example.com/boxschema/"><BoxType xmlns="http://ns.example.com/boxschema/">Box type A</BoxType></bigbox>` +
  455. ` <author xmlns="http://ns.example.com/boxschema/"><Name xmlns="http://ns.example.com/boxschema/">J.J. Johnson</Name></author>` +
  456. ` </prop>` +
  457. ` <status>HTTP/1.1 200 OK</status>` +
  458. ` </propstat>` +
  459. ` <propstat>` +
  460. ` <prop>` +
  461. ` <DingALing xmlns="http://ns.example.com/boxschema/"></DingALing>` +
  462. ` <Random xmlns="http://ns.example.com/boxschema/"></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. loop:
  557. for _, tc := range testCases {
  558. rec := httptest.NewRecorder()
  559. w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
  560. if tc.writeHeader {
  561. if err := w.writeHeader(); err != nil {
  562. t.Errorf("%s: got writeHeader error %v, want nil", tc.desc, err)
  563. continue
  564. }
  565. }
  566. for _, r := range tc.responses {
  567. if err := w.write(&r); err != nil {
  568. if err != tc.wantErr {
  569. t.Errorf("%s: got write error %v, want %v",
  570. tc.desc, err, tc.wantErr)
  571. }
  572. continue loop
  573. }
  574. }
  575. if err := w.close(); err != tc.wantErr {
  576. t.Errorf("%s: got close error %v, want %v",
  577. tc.desc, err, tc.wantErr)
  578. continue
  579. }
  580. if rec.Code != tc.wantCode {
  581. t.Errorf("%s: got HTTP status code %d, want %d\n",
  582. tc.desc, rec.Code, tc.wantCode)
  583. continue
  584. }
  585. // normalize returns the normalized XML content of s. In contrast to
  586. // the WebDAV specification, it ignores whitespace within property
  587. // values of mixed XML content.
  588. normalize := func(s string) string {
  589. d := xml.NewDecoder(strings.NewReader(s))
  590. var b bytes.Buffer
  591. e := xml.NewEncoder(&b)
  592. for {
  593. tok, err := d.Token()
  594. if err != nil {
  595. if err == io.EOF {
  596. break
  597. }
  598. t.Fatalf("%s: Token %v", tc.desc, err)
  599. }
  600. switch val := tok.(type) {
  601. case xml.Comment, xml.Directive, xml.ProcInst:
  602. continue
  603. case xml.CharData:
  604. if len(bytes.TrimSpace(val)) == 0 {
  605. continue
  606. }
  607. }
  608. if err := e.EncodeToken(tok); err != nil {
  609. t.Fatalf("%s: EncodeToken: %v", tc.desc, err)
  610. }
  611. }
  612. if err := e.Flush(); err != nil {
  613. t.Fatalf("%s: Flush: %v", tc.desc, err)
  614. }
  615. return b.String()
  616. }
  617. gotXML := normalize(rec.Body.String())
  618. wantXML := normalize(tc.wantXML)
  619. if gotXML != wantXML {
  620. t.Errorf("%s: XML body\ngot %q\nwant %q", tc.desc, gotXML, wantXML)
  621. }
  622. }
  623. }
  624. func TestReadProppatch(t *testing.T) {
  625. // TODO(rost): These "golden XML" tests easily break with changes in the
  626. // xml package. A whitespace-preserving normalizer of XML content is
  627. // required to make these tests more robust.
  628. testCases := []struct {
  629. desc string
  630. input string
  631. wantPP []Proppatch
  632. wantStatus int
  633. }{{
  634. desc: "proppatch: section 9.2",
  635. input: `` +
  636. `<?xml version="1.0" encoding="utf-8" ?>` +
  637. `<D:propertyupdate xmlns:D="DAV:"` +
  638. ` xmlns:Z="http://ns.example.com/z/">` +
  639. ` <D:set>` +
  640. ` <D:prop>` +
  641. ` <Z:Authors>` +
  642. ` <Z:Author>Jim Whitehead</Z:Author>` +
  643. ` <Z:Author>Roy Fielding</Z:Author>` +
  644. ` </Z:Authors>` +
  645. ` </D:prop>` +
  646. ` </D:set>` +
  647. ` <D:remove>` +
  648. ` <D:prop><Z:Copyright-Owner/></D:prop>` +
  649. ` </D:remove>` +
  650. `</D:propertyupdate>`,
  651. wantPP: []Proppatch{{
  652. Props: []Property{{
  653. xml.Name{Space: "http://ns.example.com/z/", Local: "Authors"},
  654. "",
  655. []byte(`` +
  656. ` ` +
  657. `<z:Author xmlns:z="http://ns.example.com/z/">` +
  658. `Jim Whitehead` +
  659. `</z:Author>` +
  660. ` ` +
  661. `<z:Author xmlns:z="http://ns.example.com/z/">` +
  662. `Roy Fielding` +
  663. `</z:Author>` +
  664. ` `,
  665. ),
  666. }},
  667. }, {
  668. Remove: true,
  669. Props: []Property{{
  670. xml.Name{Space: "http://ns.example.com/z/", Local: "Copyright-Owner"},
  671. "",
  672. nil,
  673. }},
  674. }},
  675. }, {
  676. desc: "proppatch: section 4.3.1 (mixed content)",
  677. input: `` +
  678. `<?xml version="1.0" encoding="utf-8" ?>` +
  679. `<D:propertyupdate xmlns:D="DAV:"` +
  680. ` xmlns:Z="http://ns.example.com/z/">` +
  681. ` <D:set>` +
  682. ` <D:prop xml:lang="en" xmlns:D="DAV:">` +
  683. ` <x:author xmlns:x='http://example.com/ns'>` +
  684. ` <x:name>Jane Doe</x:name>` +
  685. ` <!-- Jane's contact info -->` +
  686. ` <x:uri type='email'` +
  687. ` added='2005-11-26'>mailto:jane.doe@example.com</x:uri>` +
  688. ` <x:uri type='web'` +
  689. ` added='2005-11-27'>http://www.example.com</x:uri>` +
  690. ` <x:notes xmlns:h='http://www.w3.org/1999/xhtml'>` +
  691. ` Jane has been working way <h:em>too</h:em> long on the` +
  692. ` long-awaited revision of <![CDATA[<RFC2518>]]>.` +
  693. ` </x:notes>` +
  694. ` </x:author>` +
  695. ` </D:prop>` +
  696. ` </D:set>` +
  697. `</D:propertyupdate>`,
  698. wantPP: []Proppatch{{
  699. Props: []Property{{
  700. xml.Name{Space: "http://example.com/ns", Local: "author"},
  701. "en",
  702. []byte(`` +
  703. ` ` +
  704. `<ns:name xmlns:ns="http://example.com/ns">Jane Doe</ns:name>` +
  705. ` ` +
  706. `<ns:uri xmlns:ns="http://example.com/ns" type="email" added="2005-11-26">` +
  707. `mailto:jane.doe@example.com` +
  708. `</ns:uri>` +
  709. ` ` +
  710. `<ns:uri xmlns:ns="http://example.com/ns" type="web" added="2005-11-27">` +
  711. `http://www.example.com` +
  712. `</ns:uri>` +
  713. ` ` +
  714. `<ns:notes xmlns:ns="http://example.com/ns"` +
  715. ` xmlns:h="http://www.w3.org/1999/xhtml">` +
  716. ` ` +
  717. ` Jane has been working way` +
  718. ` <h:em>too</h:em>` +
  719. ` long on the` + ` ` +
  720. ` long-awaited revision of &lt;RFC2518&gt;.` +
  721. ` ` +
  722. `</ns:notes>` +
  723. ` `,
  724. ),
  725. }},
  726. }},
  727. }, {
  728. desc: "proppatch: lang attribute on prop",
  729. input: `` +
  730. `<?xml version="1.0" encoding="utf-8" ?>` +
  731. `<D:propertyupdate xmlns:D="DAV:">` +
  732. ` <D:set>` +
  733. ` <D:prop xml:lang="en">` +
  734. ` <foo xmlns="http://example.com/ns"/>` +
  735. ` </D:prop>` +
  736. ` </D:set>` +
  737. `</D:propertyupdate>`,
  738. wantPP: []Proppatch{{
  739. Props: []Property{{
  740. xml.Name{Space: "http://example.com/ns", Local: "foo"},
  741. "en",
  742. nil,
  743. }},
  744. }},
  745. }, {
  746. desc: "bad: remove with value",
  747. input: `` +
  748. `<?xml version="1.0" encoding="utf-8" ?>` +
  749. `<D:propertyupdate xmlns:D="DAV:"` +
  750. ` xmlns:Z="http://ns.example.com/z/">` +
  751. ` <D:remove>` +
  752. ` <D:prop>` +
  753. ` <Z:Authors>` +
  754. ` <Z:Author>Jim Whitehead</Z:Author>` +
  755. ` </Z:Authors>` +
  756. ` </D:prop>` +
  757. ` </D:remove>` +
  758. `</D:propertyupdate>`,
  759. wantStatus: http.StatusBadRequest,
  760. }, {
  761. desc: "bad: empty propertyupdate",
  762. input: `` +
  763. `<?xml version="1.0" encoding="utf-8" ?>` +
  764. `<D:propertyupdate xmlns:D="DAV:"` +
  765. `</D:propertyupdate>`,
  766. wantStatus: http.StatusBadRequest,
  767. }, {
  768. desc: "bad: empty prop",
  769. input: `` +
  770. `<?xml version="1.0" encoding="utf-8" ?>` +
  771. `<D:propertyupdate xmlns:D="DAV:"` +
  772. ` xmlns:Z="http://ns.example.com/z/">` +
  773. ` <D:remove>` +
  774. ` <D:prop/>` +
  775. ` </D:remove>` +
  776. `</D:propertyupdate>`,
  777. wantStatus: http.StatusBadRequest,
  778. }}
  779. for _, tc := range testCases {
  780. pp, status, err := readProppatch(strings.NewReader(tc.input))
  781. if tc.wantStatus != 0 {
  782. if err == nil {
  783. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  784. continue
  785. }
  786. } else if err != nil {
  787. t.Errorf("%s: %v", tc.desc, err)
  788. continue
  789. }
  790. if status != tc.wantStatus {
  791. t.Errorf("%s: got status %d, want %d", tc.desc, status, tc.wantStatus)
  792. continue
  793. }
  794. if !reflect.DeepEqual(pp, tc.wantPP) || status != tc.wantStatus {
  795. t.Errorf("%s: proppatch\ngot %v\nwant %v", tc.desc, pp, tc.wantPP)
  796. }
  797. }
  798. }