xml_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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. "encoding/xml"
  7. "net/http"
  8. "net/http/httptest"
  9. "reflect"
  10. "strings"
  11. "testing"
  12. )
  13. func TestReadLockInfo(t *testing.T) {
  14. // The "section x.y.z" test cases come from section x.y.z of the spec at
  15. // http://www.webdav.org/specs/rfc4918.html
  16. testCases := []struct {
  17. desc string
  18. input string
  19. wantLI lockInfo
  20. wantStatus int
  21. }{{
  22. "bad: junk",
  23. "xxx",
  24. lockInfo{},
  25. http.StatusBadRequest,
  26. }, {
  27. "bad: invalid owner XML",
  28. "" +
  29. "<D:lockinfo xmlns:D='DAV:'>\n" +
  30. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  31. " <D:locktype><D:write/></D:locktype>\n" +
  32. " <D:owner>\n" +
  33. " <D:href> no end tag \n" +
  34. " </D:owner>\n" +
  35. "</D:lockinfo>",
  36. lockInfo{},
  37. http.StatusBadRequest,
  38. }, {
  39. "bad: invalid UTF-8",
  40. "" +
  41. "<D:lockinfo xmlns:D='DAV:'>\n" +
  42. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  43. " <D:locktype><D:write/></D:locktype>\n" +
  44. " <D:owner>\n" +
  45. " <D:href> \xff </D:href>\n" +
  46. " </D:owner>\n" +
  47. "</D:lockinfo>",
  48. lockInfo{},
  49. http.StatusBadRequest,
  50. }, {
  51. "bad: unfinished XML #1",
  52. "" +
  53. "<D:lockinfo xmlns:D='DAV:'>\n" +
  54. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  55. " <D:locktype><D:write/></D:locktype>\n",
  56. lockInfo{},
  57. http.StatusBadRequest,
  58. }, {
  59. "bad: unfinished XML #2",
  60. "" +
  61. "<D:lockinfo xmlns:D='DAV:'>\n" +
  62. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  63. " <D:locktype><D:write/></D:locktype>\n" +
  64. " <D:owner>\n",
  65. lockInfo{},
  66. http.StatusBadRequest,
  67. }, {
  68. "good: empty",
  69. "",
  70. lockInfo{},
  71. 0,
  72. }, {
  73. "good: plain-text owner",
  74. "" +
  75. "<D:lockinfo xmlns:D='DAV:'>\n" +
  76. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  77. " <D:locktype><D:write/></D:locktype>\n" +
  78. " <D:owner>gopher</D:owner>\n" +
  79. "</D:lockinfo>",
  80. lockInfo{
  81. XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"},
  82. Exclusive: new(struct{}),
  83. Write: new(struct{}),
  84. Owner: owner{
  85. InnerXML: "gopher",
  86. },
  87. },
  88. 0,
  89. }, {
  90. "section 9.10.7",
  91. "" +
  92. "<D:lockinfo xmlns:D='DAV:'>\n" +
  93. " <D:lockscope><D:exclusive/></D:lockscope>\n" +
  94. " <D:locktype><D:write/></D:locktype>\n" +
  95. " <D:owner>\n" +
  96. " <D:href>http://example.org/~ejw/contact.html</D:href>\n" +
  97. " </D:owner>\n" +
  98. "</D:lockinfo>",
  99. lockInfo{
  100. XMLName: xml.Name{Space: "DAV:", Local: "lockinfo"},
  101. Exclusive: new(struct{}),
  102. Write: new(struct{}),
  103. Owner: owner{
  104. InnerXML: "\n <D:href>http://example.org/~ejw/contact.html</D:href>\n ",
  105. },
  106. },
  107. 0,
  108. }}
  109. for _, tc := range testCases {
  110. li, status, err := readLockInfo(strings.NewReader(tc.input))
  111. if tc.wantStatus != 0 {
  112. if err == nil {
  113. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  114. continue
  115. }
  116. } else if err != nil {
  117. t.Errorf("%s: %v", tc.desc, err)
  118. continue
  119. }
  120. if !reflect.DeepEqual(li, tc.wantLI) || status != tc.wantStatus {
  121. t.Errorf("%s:\ngot lockInfo=%v, status=%v\nwant lockInfo=%v, status=%v",
  122. tc.desc, li, status, tc.wantLI, tc.wantStatus)
  123. continue
  124. }
  125. }
  126. }
  127. func TestReadPropfind(t *testing.T) {
  128. testCases := []struct {
  129. desc string
  130. input string
  131. wantPF propfind
  132. wantStatus int
  133. }{{
  134. desc: "propfind: propname",
  135. input: "" +
  136. "<A:propfind xmlns:A='DAV:'>\n" +
  137. " <A:propname/>\n" +
  138. "</A:propfind>",
  139. wantPF: propfind{
  140. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  141. Propname: new(struct{}),
  142. },
  143. }, {
  144. desc: "propfind: empty body means allprop",
  145. input: "",
  146. wantPF: propfind{
  147. Allprop: new(struct{}),
  148. },
  149. }, {
  150. desc: "propfind: allprop",
  151. input: "" +
  152. "<A:propfind xmlns:A='DAV:'>\n" +
  153. " <A:allprop/>\n" +
  154. "</A:propfind>",
  155. wantPF: propfind{
  156. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  157. Allprop: new(struct{}),
  158. },
  159. }, {
  160. desc: "propfind: allprop followed by include",
  161. input: "" +
  162. "<A:propfind xmlns:A='DAV:'>\n" +
  163. " <A:allprop/>\n" +
  164. " <A:include><A:displayname/></A:include>\n" +
  165. "</A:propfind>",
  166. wantPF: propfind{
  167. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  168. Allprop: new(struct{}),
  169. Include: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
  170. },
  171. }, {
  172. desc: "propfind: include followed by allprop",
  173. input: "" +
  174. "<A:propfind xmlns:A='DAV:'>\n" +
  175. " <A:include><A:displayname/></A:include>\n" +
  176. " <A:allprop/>\n" +
  177. "</A:propfind>",
  178. wantPF: propfind{
  179. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  180. Allprop: new(struct{}),
  181. Include: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
  182. },
  183. }, {
  184. desc: "propfind: propfind",
  185. input: "" +
  186. "<A:propfind xmlns:A='DAV:'>\n" +
  187. " <A:prop><A:displayname/></A:prop>\n" +
  188. "</A:propfind>",
  189. wantPF: propfind{
  190. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  191. Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
  192. },
  193. }, {
  194. desc: "propfind: prop with ignored comments",
  195. input: "" +
  196. "<A:propfind xmlns:A='DAV:'>\n" +
  197. " <A:prop>\n" +
  198. " <!-- ignore -->\n" +
  199. " <A:displayname><!-- ignore --></A:displayname>\n" +
  200. " </A:prop>\n" +
  201. "</A:propfind>",
  202. wantPF: propfind{
  203. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  204. Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
  205. },
  206. }, {
  207. desc: "propfind: propfind with ignored whitespace",
  208. input: "" +
  209. "<A:propfind xmlns:A='DAV:'>\n" +
  210. " <A:prop> <A:displayname/></A:prop>\n" +
  211. "</A:propfind>",
  212. wantPF: propfind{
  213. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  214. Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
  215. },
  216. }, {
  217. desc: "propfind: propfind with ignored mixed-content",
  218. input: "" +
  219. "<A:propfind xmlns:A='DAV:'>\n" +
  220. " <A:prop>foo<A:displayname/>bar</A:prop>\n" +
  221. "</A:propfind>",
  222. wantPF: propfind{
  223. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  224. Prop: propnames{xml.Name{Space: "DAV:", Local: "displayname"}},
  225. },
  226. }, {
  227. desc: "propfind: propname with ignored element (section A.4)",
  228. input: "" +
  229. "<A:propfind xmlns:A='DAV:'>\n" +
  230. " <A:propname/>\n" +
  231. " <E:leave-out xmlns:E='E:'>*boss*</E:leave-out>\n" +
  232. "</A:propfind>",
  233. wantPF: propfind{
  234. XMLName: xml.Name{Space: "DAV:", Local: "propfind"},
  235. Propname: new(struct{}),
  236. },
  237. }, {
  238. desc: "propfind: bad: junk",
  239. input: "xxx",
  240. wantStatus: http.StatusBadRequest,
  241. }, {
  242. desc: "propfind: bad: propname and allprop (section A.3)",
  243. input: "" +
  244. "<A:propfind xmlns:A='DAV:'>\n" +
  245. " <A:propname/>" +
  246. " <A:allprop/>" +
  247. "</A:propfind>",
  248. wantStatus: http.StatusBadRequest,
  249. }, {
  250. desc: "propfind: bad: propname and prop",
  251. input: "" +
  252. "<A:propfind xmlns:A='DAV:'>\n" +
  253. " <A:prop><A:displayname/></A:prop>\n" +
  254. " <A:propname/>\n" +
  255. "</A:propfind>",
  256. wantStatus: http.StatusBadRequest,
  257. }, {
  258. desc: "propfind: bad: allprop and prop",
  259. input: "" +
  260. "<A:propfind xmlns:A='DAV:'>\n" +
  261. " <A:allprop/>\n" +
  262. " <A:prop><A:foo/><A:/prop>\n" +
  263. "</A:propfind>",
  264. wantStatus: http.StatusBadRequest,
  265. }, {
  266. desc: "propfind: bad: empty propfind with ignored element (section A.4)",
  267. input: "" +
  268. "<A:propfind xmlns:A='DAV:'>\n" +
  269. " <E:expired-props/>\n" +
  270. "</A:propfind>",
  271. wantStatus: http.StatusBadRequest,
  272. }, {
  273. desc: "propfind: bad: empty prop",
  274. input: "" +
  275. "<A:propfind xmlns:A='DAV:'>\n" +
  276. " <A:prop/>\n" +
  277. "</A:propfind>",
  278. wantStatus: http.StatusBadRequest,
  279. }, {
  280. desc: "propfind: bad: prop with just chardata",
  281. input: "" +
  282. "<A:propfind xmlns:A='DAV:'>\n" +
  283. " <A:prop>foo</A:prop>\n" +
  284. "</A:propfind>",
  285. wantStatus: http.StatusBadRequest,
  286. }, {
  287. desc: "bad: interrupted prop",
  288. input: "" +
  289. "<A:propfind xmlns:A='DAV:'>\n" +
  290. " <A:prop><A:foo></A:prop>\n",
  291. wantStatus: http.StatusBadRequest,
  292. }, {
  293. desc: "bad: malformed end element prop",
  294. input: "" +
  295. "<A:propfind xmlns:A='DAV:'>\n" +
  296. " <A:prop><A:foo/></A:bar></A:prop>\n",
  297. wantStatus: http.StatusBadRequest,
  298. }, {
  299. desc: "propfind: bad: property with chardata value",
  300. input: "" +
  301. "<A:propfind xmlns:A='DAV:'>\n" +
  302. " <A:prop><A:foo>bar</A:foo></A:prop>\n" +
  303. "</A:propfind>",
  304. wantStatus: http.StatusBadRequest,
  305. }, {
  306. desc: "propfind: bad: property with whitespace value",
  307. input: "" +
  308. "<A:propfind xmlns:A='DAV:'>\n" +
  309. " <A:prop><A:foo> </A:foo></A:prop>\n" +
  310. "</A:propfind>",
  311. wantStatus: http.StatusBadRequest,
  312. }, {
  313. desc: "propfind: bad: include without allprop",
  314. input: "" +
  315. "<A:propfind xmlns:A='DAV:'>\n" +
  316. " <A:include><A:foo/></A:include>\n" +
  317. "</A:propfind>",
  318. wantStatus: http.StatusBadRequest,
  319. }}
  320. for _, tc := range testCases {
  321. pf, status, err := readPropfind(strings.NewReader(tc.input))
  322. if tc.wantStatus != 0 {
  323. if err == nil {
  324. t.Errorf("%s: got nil error, want non-nil", tc.desc)
  325. continue
  326. }
  327. } else if err != nil {
  328. t.Errorf("%s: %v", tc.desc, err)
  329. continue
  330. }
  331. if !reflect.DeepEqual(pf, tc.wantPF) || status != tc.wantStatus {
  332. t.Errorf("%s:\ngot propfind=%v, status=%v\nwant propfind=%v, status=%v",
  333. tc.desc, pf, status, tc.wantPF, tc.wantStatus)
  334. continue
  335. }
  336. }
  337. }
  338. func TestMultistatusWriter(t *testing.T) {
  339. ///The "section x.y.z" test cases come from section x.y.z of the spec at
  340. // http://www.webdav.org/specs/rfc4918.html
  341. //
  342. // BUG:The following tests compare the actual and expected XML verbatim.
  343. // Minor tweaks in the marshalling output of either standard encoding/xml
  344. // or this package might break them. A more resilient approach could be
  345. // to normalize both actual and expected XML content before comparison.
  346. // This also would enhance readibility of the expected XML payload in the
  347. // wantXML field.
  348. testCases := []struct {
  349. desc string
  350. responses []response
  351. respdesc string
  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{Space: "http://ns.example.com/", Local: "Authors"},
  362. }},
  363. Status: "HTTP/1.1 424 Failed Dependency",
  364. }, {
  365. Prop: []Property{{
  366. XMLName: xml.Name{Space: "http://ns.example.com/", Local: "Copyright-Owner"},
  367. }},
  368. Status: "HTTP/1.1 409 Conflict",
  369. }},
  370. ResponseDescription: " Copyright Owner cannot be deleted or altered.",
  371. }},
  372. wantXML: `<?xml version="1.0" encoding="UTF-8"?>` +
  373. `<D:multistatus xmlns:D="DAV:">` +
  374. `<response xmlns="DAV:">` +
  375. `<href xmlns="DAV:">http://example.com/foo</href>` +
  376. `<propstat xmlns="DAV:">` +
  377. `<prop>` +
  378. `<Authors xmlns="http://ns.example.com/"></Authors>` +
  379. `</prop>` +
  380. `<status xmlns="DAV:">HTTP/1.1 424 Failed Dependency</status>` +
  381. `</propstat>` +
  382. `<propstat xmlns="DAV:">` +
  383. `<prop>` +
  384. `<Copyright-Owner xmlns="http://ns.example.com/"></Copyright-Owner>` +
  385. `</prop>` +
  386. `<status xmlns="DAV:">HTTP/1.1 409 Conflict</status>` +
  387. `</propstat>` +
  388. `<responsedescription xmlns="DAV:">` +
  389. ` Copyright Owner cannot be deleted or altered.` +
  390. `</responsedescription>` +
  391. `</response>` +
  392. `</D:multistatus>`,
  393. wantCode: StatusMulti,
  394. }, {
  395. desc: "section 9.6.2 (lock-token-submitted)",
  396. responses: []response{{
  397. Href: []string{"http://example.com/foo"},
  398. Status: "HTTP/1.1 423 Locked",
  399. Error: &xmlError{
  400. InnerXML: []byte(`<lock-token-submitted xmlns="DAV:"/>`),
  401. },
  402. }},
  403. wantXML: `<?xml version="1.0" encoding="UTF-8"?>` +
  404. `<D:multistatus xmlns:D="DAV:">` +
  405. `<response xmlns="DAV:">` +
  406. `<href xmlns="DAV:">http://example.com/foo</href>` +
  407. `<status xmlns="DAV:">HTTP/1.1 423 Locked</status>` +
  408. `<error xmlns="DAV:"><lock-token-submitted xmlns="DAV:"/></error>` +
  409. `</response>` +
  410. `</D:multistatus>`,
  411. wantCode: StatusMulti,
  412. }, {
  413. desc: "section 9.1.3",
  414. responses: []response{{
  415. Href: []string{"http://example.com/foo"},
  416. Propstat: []propstat{{
  417. Prop: []Property{{
  418. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "bigbox"},
  419. InnerXML: []byte(`` +
  420. `<BoxType xmlns="http://ns.example.com/boxschema/">` +
  421. `Box type A` +
  422. `</BoxType>`),
  423. }, {
  424. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "author"},
  425. InnerXML: []byte(`` +
  426. `<Name xmlns="http://ns.example.com/boxschema/">` +
  427. `J.J. Johnson` +
  428. `</Name>`),
  429. }},
  430. Status: "HTTP/1.1 200 OK",
  431. }, {
  432. Prop: []Property{{
  433. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "DingALing"},
  434. }, {
  435. XMLName: xml.Name{Space: "http://ns.example.com/boxschema/", Local: "Random"},
  436. }},
  437. Status: "HTTP/1.1 403 Forbidden",
  438. ResponseDescription: " The user does not have access to the DingALing property.",
  439. }},
  440. }},
  441. respdesc: " There has been an access violation error.",
  442. wantXML: `<?xml version="1.0" encoding="UTF-8"?>` +
  443. `<D:multistatus xmlns:D="DAV:">` +
  444. `<response xmlns="DAV:">` +
  445. `<href xmlns="DAV:">http://example.com/foo</href>` +
  446. `<propstat xmlns="DAV:">` +
  447. `<prop>` +
  448. `<bigbox xmlns="http://ns.example.com/boxschema/">` +
  449. `<BoxType xmlns="http://ns.example.com/boxschema/">Box type A</BoxType>` +
  450. `</bigbox>` +
  451. `<author xmlns="http://ns.example.com/boxschema/">` +
  452. `<Name xmlns="http://ns.example.com/boxschema/">J.J. Johnson</Name>` +
  453. `</author>` +
  454. `</prop>` +
  455. `<status xmlns="DAV:">HTTP/1.1 200 OK</status>` +
  456. `</propstat>` +
  457. `<propstat xmlns="DAV:">` +
  458. `<prop>` +
  459. `<DingALing xmlns="http://ns.example.com/boxschema/">` +
  460. `</DingALing>` +
  461. `<Random xmlns="http://ns.example.com/boxschema/">` +
  462. `</Random>` +
  463. `</prop>` +
  464. `<status xmlns="DAV:">HTTP/1.1 403 Forbidden</status>` +
  465. `<responsedescription xmlns="DAV:">` +
  466. ` The user does not have access to the DingALing property.` +
  467. `</responsedescription>` +
  468. `</propstat>` +
  469. `</response>` +
  470. `<D:responsedescription>` +
  471. ` There has been an access violation error.` +
  472. `</D:responsedescription>` +
  473. `</D:multistatus>`,
  474. wantCode: StatusMulti,
  475. }, {
  476. desc: "bad: no response written",
  477. // default of http.responseWriter
  478. wantCode: http.StatusOK,
  479. }, {
  480. desc: "bad: no response written (with description)",
  481. respdesc: "too bad",
  482. // default of http.responseWriter
  483. wantCode: http.StatusOK,
  484. }, {
  485. desc: "bad: no href",
  486. responses: []response{{
  487. Propstat: []propstat{{
  488. Prop: []Property{{
  489. XMLName: xml.Name{Space: "http://example.com/", Local: "foo"},
  490. }},
  491. Status: "HTTP/1.1 200 OK",
  492. }},
  493. }},
  494. wantErr: errInvalidResponse,
  495. // default of http.responseWriter
  496. wantCode: http.StatusOK,
  497. }, {
  498. desc: "bad: multiple hrefs and no status",
  499. responses: []response{{
  500. Href: []string{"http://example.com/foo", "http://example.com/bar"},
  501. }},
  502. wantErr: errInvalidResponse,
  503. // default of http.responseWriter
  504. wantCode: http.StatusOK,
  505. }, {
  506. desc: "bad: one href and no propstat",
  507. responses: []response{{
  508. Href: []string{"http://example.com/foo"},
  509. }},
  510. wantErr: errInvalidResponse,
  511. // default of http.responseWriter
  512. wantCode: http.StatusOK,
  513. }, {
  514. desc: "bad: status with one href and propstat",
  515. responses: []response{{
  516. Href: []string{"http://example.com/foo"},
  517. Propstat: []propstat{{
  518. Prop: []Property{{
  519. XMLName: xml.Name{Space: "http://example.com/", Local: "foo"},
  520. }},
  521. Status: "HTTP/1.1 200 OK",
  522. }},
  523. Status: "HTTP/1.1 200 OK",
  524. }},
  525. wantErr: errInvalidResponse,
  526. // default of http.responseWriter
  527. wantCode: http.StatusOK,
  528. }, {
  529. desc: "bad: multiple hrefs and propstat",
  530. responses: []response{{
  531. Href: []string{"http://example.com/foo", "http://example.com/bar"},
  532. Propstat: []propstat{{
  533. Prop: []Property{{
  534. XMLName: xml.Name{Space: "http://example.com/", Local: "foo"},
  535. }},
  536. Status: "HTTP/1.1 200 OK",
  537. }},
  538. }},
  539. wantErr: errInvalidResponse,
  540. // default of http.responseWriter
  541. wantCode: http.StatusOK,
  542. }}
  543. loop:
  544. for _, tc := range testCases {
  545. rec := httptest.NewRecorder()
  546. w := multistatusWriter{w: rec, responseDescription: tc.respdesc}
  547. for _, r := range tc.responses {
  548. if err := w.write(&r); err != nil {
  549. if err != tc.wantErr {
  550. t.Errorf("%s: got write error %v, want %v", tc.desc, err, tc.wantErr)
  551. }
  552. continue loop
  553. }
  554. }
  555. if err := w.close(); err != tc.wantErr {
  556. t.Errorf("%s: got close error %v, want %v", tc.desc, err, tc.wantErr)
  557. continue
  558. }
  559. if rec.Code != tc.wantCode {
  560. t.Errorf("%s: got HTTP status code %d, want %d\n", tc.desc, rec.Code, tc.wantCode)
  561. continue
  562. }
  563. if gotXML := rec.Body.String(); gotXML != tc.wantXML {
  564. t.Errorf("%s: XML body\ngot %q\nwant %q", tc.desc, gotXML, tc.wantXML)
  565. continue
  566. }
  567. }
  568. }