xml_test.go 23 KB

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