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