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