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